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}