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 839286662405d95bea7f876cb90d0e0ab28a684f 193 lines 4.9 kB view raw
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 const entries: Entry[] = []; 26 let fileCount = 0; 27 28 // Group files by directory 29 const directoryMap = new Map<string, UploadedFile[]>(); 30 31 for (const file of files) { 32 // Remove any base folder name from the path 33 const normalizedPath = file.name.replace(/^[^\/]*\//, ''); 34 const parts = normalizedPath.split('/'); 35 36 if (parts.length === 1) { 37 // Root level file 38 entries.push({ 39 name: parts[0], 40 node: { 41 $type: 'place.wisp.fs#file' as const, 42 type: 'file' as const, 43 blob: undefined as any // Will be filled after upload 44 } 45 }); 46 fileCount++; 47 } else { 48 // File in subdirectory 49 const dirPath = parts.slice(0, -1).join('/'); 50 if (!directoryMap.has(dirPath)) { 51 directoryMap.set(dirPath, []); 52 } 53 directoryMap.get(dirPath)!.push({ 54 ...file, 55 name: normalizedPath 56 }); 57 } 58 } 59 60 // Process subdirectories 61 for (const [dirPath, dirFiles] of directoryMap) { 62 const dirEntries: Entry[] = []; 63 64 for (const file of dirFiles) { 65 const fileName = file.name.split('/').pop()!; 66 dirEntries.push({ 67 name: fileName, 68 node: { 69 $type: 'place.wisp.fs#file' as const, 70 type: 'file' as const, 71 blob: undefined as any // Will be filled after upload 72 } 73 }); 74 fileCount++; 75 } 76 77 // Build nested directory structure 78 const pathParts = dirPath.split('/'); 79 let currentEntries = entries; 80 81 for (let i = 0; i < pathParts.length; i++) { 82 const part = pathParts[i]; 83 const isLast = i === pathParts.length - 1; 84 85 let existingEntry = currentEntries.find(e => e.name === part); 86 87 if (!existingEntry) { 88 const newDir = { 89 $type: 'place.wisp.fs#directory' as const, 90 type: 'directory' as const, 91 entries: isLast ? dirEntries : [] 92 }; 93 94 existingEntry = { 95 name: part, 96 node: newDir 97 }; 98 currentEntries.push(existingEntry); 99 } else if ('entries' in existingEntry.node && isLast) { 100 (existingEntry.node as any).entries.push(...dirEntries); 101 } 102 103 if (existingEntry && 'entries' in existingEntry.node) { 104 currentEntries = (existingEntry.node as any).entries; 105 } 106 } 107 } 108 109 const result = { 110 directory: { 111 $type: 'place.wisp.fs#directory' as const, 112 type: 'directory' as const, 113 entries 114 }, 115 fileCount 116 }; 117 118 return result; 119} 120 121/** 122 * Create the manifest record for a site 123 */ 124export function createManifest( 125 siteName: string, 126 root: Directory, 127 fileCount: number 128): Record { 129 return { 130 $type: 'place.wisp.fs' as const, 131 site: siteName, 132 root, 133 fileCount, 134 createdAt: new Date().toISOString() 135 }; 136} 137 138/** 139 * Update file blobs in directory structure after upload 140 * Uses path-based matching to correctly match files in nested directories 141 */ 142export function updateFileBlobs( 143 directory: Directory, 144 uploadResults: FileUploadResult[], 145 filePaths: string[], 146 currentPath: string = '' 147): Directory { 148 const updatedEntries = directory.entries.map(entry => { 149 if ('type' in entry.node && entry.node.type === 'file') { 150 // Build the full path for this file 151 const fullPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; 152 153 // Find exact match in filePaths (need to handle normalized paths) 154 const fileIndex = filePaths.findIndex((path) => { 155 // Normalize both paths by removing leading base folder 156 const normalizedUploadPath = path.replace(/^[^\/]*\//, ''); 157 const normalizedEntryPath = fullPath; 158 return normalizedUploadPath === normalizedEntryPath || path === fullPath; 159 }); 160 161 if (fileIndex !== -1 && uploadResults[fileIndex]) { 162 const blobRef = uploadResults[fileIndex].blobRef; 163 164 return { 165 ...entry, 166 node: { 167 $type: 'place.wisp.fs#file' as const, 168 type: 'file' as const, 169 blob: blobRef 170 } 171 }; 172 } else { 173 console.error(`❌ BLOB MATCHING ERROR: Could not find blob for file: ${fullPath}`); 174 console.error(` Available paths:`, filePaths.slice(0, 10), filePaths.length > 10 ? `... and ${filePaths.length - 10} more` : ''); 175 } 176 } else if ('type' in entry.node && entry.node.type === 'directory') { 177 const dirPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; 178 return { 179 ...entry, 180 node: updateFileBlobs(entry.node as Directory, uploadResults, filePaths, dirPath) 181 }; 182 } 183 return entry; 184 }) as Entry[]; 185 186 const result = { 187 $type: 'place.wisp.fs#directory' as const, 188 type: 'directory' as const, 189 entries: updatedEntries 190 }; 191 192 return result; 193}