Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import type { BlobRef } from "@atproto/api"; 2import type { Record, Directory, File, Entry } from "../lexicon/types/place/wisp/fs"; 3 4export interface UploadedFile { 5 name: string; 6 content: Buffer; 7 mimeType: string; 8 size: number; 9} 10 11export interface FileUploadResult { 12 hash: string; 13 blobRef: BlobRef; 14} 15 16export interface ProcessedDirectory { 17 directory: Directory; 18 fileCount: number; 19} 20 21/** 22 * Process uploaded files into a directory structure 23 */ 24export function processUploadedFiles(files: UploadedFile[]): ProcessedDirectory { 25 console.log(`🏗️ Processing ${files.length} uploaded files`); 26 const entries: Entry[] = []; 27 let fileCount = 0; 28 29 // Group files by directory 30 const directoryMap = new Map<string, UploadedFile[]>(); 31 32 for (const file of files) { 33 // Remove any base folder name from the path 34 const normalizedPath = file.name.replace(/^[^\/]*\//, ''); 35 const parts = normalizedPath.split('/'); 36 37 console.log(`📄 Processing file: ${file.name} -> normalized: ${normalizedPath}`); 38 39 if (parts.length === 1) { 40 // Root level file 41 console.log(`📁 Root level file: ${parts[0]}`); 42 entries.push({ 43 name: parts[0], 44 node: { 45 $type: 'place.wisp.fs#file' as const, 46 type: 'file' as const, 47 blob: undefined as any // Will be filled after upload 48 } 49 }); 50 fileCount++; 51 } else { 52 // File in subdirectory 53 const dirPath = parts.slice(0, -1).join('/'); 54 console.log(`📂 Subdirectory file: ${dirPath}/${parts[parts.length - 1]}`); 55 if (!directoryMap.has(dirPath)) { 56 directoryMap.set(dirPath, []); 57 console.log(`➕ Created directory: ${dirPath}`); 58 } 59 directoryMap.get(dirPath)!.push({ 60 ...file, 61 name: normalizedPath 62 }); 63 } 64 } 65 66 // Process subdirectories 67 console.log(`📂 Processing ${directoryMap.size} subdirectories`); 68 for (const [dirPath, dirFiles] of directoryMap) { 69 console.log(`📁 Processing directory: ${dirPath} with ${dirFiles.length} files`); 70 const dirEntries: Entry[] = []; 71 72 for (const file of dirFiles) { 73 const fileName = file.name.split('/').pop()!; 74 console.log(` 📄 Adding file to directory: ${fileName}`); 75 dirEntries.push({ 76 name: fileName, 77 node: { 78 $type: 'place.wisp.fs#file' as const, 79 type: 'file' as const, 80 blob: undefined as any // Will be filled after upload 81 } 82 }); 83 fileCount++; 84 } 85 86 // Build nested directory structure 87 const pathParts = dirPath.split('/'); 88 let currentEntries = entries; 89 90 console.log(`🏗️ Building nested structure for path: ${pathParts.join('/')}`); 91 92 for (let i = 0; i < pathParts.length; i++) { 93 const part = pathParts[i]; 94 const isLast = i === pathParts.length - 1; 95 96 let existingEntry = currentEntries.find(e => e.name === part); 97 98 if (!existingEntry) { 99 const newDir = { 100 $type: 'place.wisp.fs#directory' as const, 101 type: 'directory' as const, 102 entries: isLast ? dirEntries : [] 103 }; 104 105 existingEntry = { 106 name: part, 107 node: newDir 108 }; 109 currentEntries.push(existingEntry); 110 console.log(` ➕ Created directory entry: ${part}`); 111 } else if ('entries' in existingEntry.node && isLast) { 112 (existingEntry.node as any).entries.push(...dirEntries); 113 console.log(` 📝 Added files to existing directory: ${part}`); 114 } 115 116 if (existingEntry && 'entries' in existingEntry.node) { 117 currentEntries = (existingEntry.node as any).entries; 118 } 119 } 120 } 121 122 console.log(`✅ Directory structure completed with ${fileCount} total files`); 123 124 const result = { 125 directory: { 126 $type: 'place.wisp.fs#directory' as const, 127 type: 'directory' as const, 128 entries 129 }, 130 fileCount 131 }; 132 133 console.log('📋 Final directory structure:', JSON.stringify(result, null, 2)); 134 return result; 135} 136 137/** 138 * Create the manifest record for a site 139 */ 140export function createManifest( 141 siteName: string, 142 root: Directory, 143 fileCount: number 144): Record { 145 const manifest: Record = { 146 $type: 'place.wisp.fs' as const, 147 site: siteName, 148 root, 149 fileCount, 150 createdAt: new Date().toISOString() 151 }; 152 153 console.log(`📋 Created manifest for site "${siteName}" with ${fileCount} files`); 154 console.log('📄 Manifest structure:', JSON.stringify(manifest, null, 2)); 155 156 return manifest; 157} 158 159/** 160 * Update file blobs in directory structure after upload 161 */ 162export function updateFileBlobs( 163 directory: Directory, 164 uploadResults: FileUploadResult[], 165 filePaths: string[] 166): Directory { 167 console.log(`🔄 Updating file blobs: ${uploadResults.length} results for ${filePaths.length} paths`); 168 169 const updatedEntries = directory.entries.map(entry => { 170 if ('type' in entry.node && entry.node.type === 'file') { 171 const fileIndex = filePaths.findIndex(path => path.endsWith(entry.name)); 172 if (fileIndex !== -1 && uploadResults[fileIndex]) { 173 console.log(` 🔗 Updating blob for file: ${entry.name} -> ${uploadResults[fileIndex].hash}`); 174 return { 175 ...entry, 176 node: { 177 $type: 'place.wisp.fs#file' as const, 178 type: 'file' as const, 179 blob: uploadResults[fileIndex].blobRef 180 } 181 }; 182 } else { 183 console.warn(` ⚠️ Could not find upload result for file: ${entry.name}`); 184 } 185 } else if ('type' in entry.node && entry.node.type === 'directory') { 186 console.log(` 📂 Recursively updating directory: ${entry.name}`); 187 return { 188 ...entry, 189 node: updateFileBlobs(entry.node as Directory, uploadResults, filePaths) 190 }; 191 } 192 return entry; 193 }) as Entry[]; 194 195 const result = { 196 $type: 'place.wisp.fs#directory' as const, 197 type: 'directory' as const, 198 entries: updatedEntries 199 }; 200 201 console.log('✅ File blobs updated'); 202 return result; 203}