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

Configure Feed

Select the types of activity you want to include in your feed.

at 585052729a5668311d0125c904facbdae085a648 241 lines 6.3 kB view raw
1import type { BlobRef } from "@atproto/api"; 2import type { Record, Directory, File, Entry } from "../lexicon/types/place/wisp/fs"; 3import { validateRecord } from "../lexicon/types/place/wisp/fs"; 4import { gzipSync } from 'zlib'; 5 6export interface UploadedFile { 7 name: string; 8 content: Buffer; 9 mimeType: string; 10 size: number; 11 compressed?: boolean; 12 originalMimeType?: string; 13} 14 15export interface FileUploadResult { 16 hash: string; 17 blobRef: BlobRef; 18 encoding?: 'gzip'; 19 mimeType?: string; 20 base64?: boolean; 21} 22 23export interface ProcessedDirectory { 24 directory: Directory; 25 fileCount: number; 26} 27 28/** 29 * Determine if a file should be gzip compressed based on its MIME type 30 */ 31export function shouldCompressFile(mimeType: string): boolean { 32 // Compress text-based files 33 const compressibleTypes = [ 34 'text/html', 35 'text/css', 36 'text/javascript', 37 'application/javascript', 38 'application/json', 39 'image/svg+xml', 40 'text/xml', 41 'application/xml', 42 'text/plain', 43 'application/x-javascript' 44 ]; 45 46 // Check if mime type starts with any compressible type 47 return compressibleTypes.some(type => mimeType.startsWith(type)); 48} 49 50/** 51 * Compress a file using gzip 52 */ 53export function compressFile(content: Buffer): Buffer { 54 return gzipSync(content, { level: 9 }); 55} 56 57/** 58 * Process uploaded files into a directory structure 59 */ 60export function processUploadedFiles(files: UploadedFile[]): ProcessedDirectory { 61 const entries: Entry[] = []; 62 let fileCount = 0; 63 64 // Group files by directory 65 const directoryMap = new Map<string, UploadedFile[]>(); 66 67 for (const file of files) { 68 // Remove any base folder name from the path 69 const normalizedPath = file.name.replace(/^[^\/]*\//, ''); 70 const parts = normalizedPath.split('/'); 71 72 if (parts.length === 1) { 73 // Root level file 74 entries.push({ 75 name: parts[0], 76 node: { 77 $type: 'place.wisp.fs#file' as const, 78 type: 'file' as const, 79 blob: undefined as any // Will be filled after upload 80 } 81 }); 82 fileCount++; 83 } else { 84 // File in subdirectory 85 const dirPath = parts.slice(0, -1).join('/'); 86 if (!directoryMap.has(dirPath)) { 87 directoryMap.set(dirPath, []); 88 } 89 directoryMap.get(dirPath)!.push({ 90 ...file, 91 name: normalizedPath 92 }); 93 } 94 } 95 96 // Process subdirectories 97 for (const [dirPath, dirFiles] of directoryMap) { 98 const dirEntries: Entry[] = []; 99 100 for (const file of dirFiles) { 101 const fileName = file.name.split('/').pop()!; 102 dirEntries.push({ 103 name: fileName, 104 node: { 105 $type: 'place.wisp.fs#file' as const, 106 type: 'file' as const, 107 blob: undefined as any // Will be filled after upload 108 } 109 }); 110 fileCount++; 111 } 112 113 // Build nested directory structure 114 const pathParts = dirPath.split('/'); 115 let currentEntries = entries; 116 117 for (let i = 0; i < pathParts.length; i++) { 118 const part = pathParts[i]; 119 const isLast = i === pathParts.length - 1; 120 121 let existingEntry = currentEntries.find(e => e.name === part); 122 123 if (!existingEntry) { 124 const newDir = { 125 $type: 'place.wisp.fs#directory' as const, 126 type: 'directory' as const, 127 entries: isLast ? dirEntries : [] 128 }; 129 130 existingEntry = { 131 name: part, 132 node: newDir 133 }; 134 currentEntries.push(existingEntry); 135 } else if ('entries' in existingEntry.node && isLast) { 136 (existingEntry.node as any).entries.push(...dirEntries); 137 } 138 139 if (existingEntry && 'entries' in existingEntry.node) { 140 currentEntries = (existingEntry.node as any).entries; 141 } 142 } 143 } 144 145 const result = { 146 directory: { 147 $type: 'place.wisp.fs#directory' as const, 148 type: 'directory' as const, 149 entries 150 }, 151 fileCount 152 }; 153 154 return result; 155} 156 157/** 158 * Create the manifest record for a site 159 */ 160export function createManifest( 161 siteName: string, 162 root: Directory, 163 fileCount: number 164): Record { 165 const manifest = { 166 $type: 'place.wisp.fs' as const, 167 site: siteName, 168 root, 169 fileCount, 170 createdAt: new Date().toISOString() 171 }; 172 173 // Validate the manifest before returning 174 const validationResult = validateRecord(manifest); 175 if (!validationResult.success) { 176 throw new Error(`Invalid manifest: ${validationResult.error?.message || 'Validation failed'}`); 177 } 178 179 return manifest; 180} 181 182/** 183 * Update file blobs in directory structure after upload 184 * Uses path-based matching to correctly match files in nested directories 185 */ 186export function updateFileBlobs( 187 directory: Directory, 188 uploadResults: FileUploadResult[], 189 filePaths: string[], 190 currentPath: string = '' 191): Directory { 192 const updatedEntries = directory.entries.map(entry => { 193 if ('type' in entry.node && entry.node.type === 'file') { 194 // Build the full path for this file 195 const fullPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; 196 197 // Find exact match in filePaths (need to handle normalized paths) 198 const fileIndex = filePaths.findIndex((path) => { 199 // Normalize both paths by removing leading base folder 200 const normalizedUploadPath = path.replace(/^[^\/]*\//, ''); 201 const normalizedEntryPath = fullPath; 202 return normalizedUploadPath === normalizedEntryPath || path === fullPath; 203 }); 204 205 if (fileIndex !== -1 && uploadResults[fileIndex]) { 206 const result = uploadResults[fileIndex]; 207 const blobRef = result.blobRef; 208 209 return { 210 ...entry, 211 node: { 212 $type: 'place.wisp.fs#file' as const, 213 type: 'file' as const, 214 blob: blobRef, 215 ...(result.encoding && { encoding: result.encoding }), 216 ...(result.mimeType && { mimeType: result.mimeType }), 217 ...(result.base64 && { base64: result.base64 }) 218 } 219 }; 220 } else { 221 console.error(`❌ BLOB MATCHING ERROR: Could not find blob for file: ${fullPath}`); 222 console.error(` Available paths:`, filePaths.slice(0, 10), filePaths.length > 10 ? `... and ${filePaths.length - 10} more` : ''); 223 } 224 } else if ('type' in entry.node && entry.node.type === 'directory') { 225 const dirPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; 226 return { 227 ...entry, 228 node: updateFileBlobs(entry.node as Directory, uploadResults, filePaths, dirPath) 229 }; 230 } 231 return entry; 232 }) as Entry[]; 233 234 const result = { 235 $type: 'place.wisp.fs#directory' as const, 236 type: 'directory' as const, 237 entries: updatedEntries 238 }; 239 240 return result; 241}