tangled
alpha
login
or
join now
nekomimi.pet
/
wisp.place-monorepo
89
fork
atom
Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
89
fork
atom
overview
issues
10
pulls
pipelines
Fix directory index routing
nekomimi.pet
2 weeks ago
3e297fcd
45e5d79f
1/2
deploy-wisp.yml
success
38s
test.yml
failed
30s
+54
-21
2 changed files
expand all
collapse all
unified
split
apps
hosting-service
src
lib
file-serving.ts
packages
@wispplace
tiered-storage
src
tiers
DiskStorageTier.ts
+4
-2
apps/hosting-service/src/lib/file-serving.ts
···
383
};
384
385
const indexFiles = getIndexFiles(settings);
0
386
387
// Normalize the request path (keep empty for root, remove trailing slash for others)
388
let requestPath = filePath || '';
···
393
// For directory-like paths (empty or no file extension in basename), try index files
394
if (!requestPath || !hasFileExtension(requestPath)) {
395
// For non-empty extensionless paths, try as a direct file first (e.g. binary downloads)
396
-
if (requestPath) {
397
const directResult = await span(trace, `storage:${requestPath}`, () => getFileWithMetadata(did, rkey, requestPath));
398
if (directResult) {
399
return buildResponseFromStorageResult(directResult, requestPath, settings, requestHeaders);
···
694
};
695
696
const indexFiles = getIndexFiles(settings);
0
697
const buildResponse = (fileResult: FileForRequestResult): Response => {
698
const meta = fileResult.result.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined;
699
const mimeType = meta?.mimeType || lookup(fileResult.filePath) || 'application/octet-stream';
···
716
// For directory-like paths (empty or no file extension in basename), try index files
717
if (!requestPath || !hasFileExtension(requestPath)) {
718
// For non-empty extensionless paths, try as a direct file first (e.g. binary downloads)
719
-
if (requestPath) {
720
const directResult = await span(trace, `storage:${requestPath}`, () => getFileForRequest(did, rkey, requestPath, true));
721
if (directResult) {
722
return buildResponse(directResult);
···
383
};
384
385
const indexFiles = getIndexFiles(settings);
386
+
const isDirectoryPathRequest = filePath.endsWith('/') && filePath.length > 0;
387
388
// Normalize the request path (keep empty for root, remove trailing slash for others)
389
let requestPath = filePath || '';
···
394
// For directory-like paths (empty or no file extension in basename), try index files
395
if (!requestPath || !hasFileExtension(requestPath)) {
396
// For non-empty extensionless paths, try as a direct file first (e.g. binary downloads)
397
+
if (requestPath && !isDirectoryPathRequest) {
398
const directResult = await span(trace, `storage:${requestPath}`, () => getFileWithMetadata(did, rkey, requestPath));
399
if (directResult) {
400
return buildResponseFromStorageResult(directResult, requestPath, settings, requestHeaders);
···
695
};
696
697
const indexFiles = getIndexFiles(settings);
698
+
const isDirectoryPathRequest = filePath.endsWith('/') && filePath.length > 0;
699
const buildResponse = (fileResult: FileForRequestResult): Response => {
700
const meta = fileResult.result.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined;
701
const mimeType = meta?.mimeType || lookup(fileResult.filePath) || 'application/octet-stream';
···
718
// For directory-like paths (empty or no file extension in basename), try index files
719
if (!requestPath || !hasFileExtension(requestPath)) {
720
// For non-empty extensionless paths, try as a direct file first (e.g. binary downloads)
721
+
if (requestPath && !isDirectoryPathRequest) {
722
const directResult = await span(trace, `storage:${requestPath}`, () => getFileForRequest(did, rkey, requestPath, true));
723
if (directResult) {
724
return buildResponse(directResult);
+50
-19
packages/@wispplace/tiered-storage/src/tiers/DiskStorageTier.ts
···
11
} from '../types/index.js';
12
import { encodeKey, decodeKey } from '../utils/path-encoding.js';
13
0
0
0
0
0
0
14
/**
15
* Eviction policy for disk tier when size limit is reached.
16
*/
···
180
const data = await readFile(filePath);
181
return new Uint8Array(data);
182
} catch (error) {
183
-
const code = (error as NodeJS.ErrnoException).code;
184
-
if (code === 'ENOENT' || code === 'ENOTDIR') {
185
return null;
186
}
187
throw error;
···
222
223
return { data: new Uint8Array(dataBuffer), metadata };
224
} catch (error) {
225
-
const code = (error as NodeJS.ErrnoException).code;
226
-
if (code === 'ENOENT' || code === 'ENOTDIR') {
227
return null;
228
}
229
if (error instanceof SyntaxError) {
···
262
metadata.ttl = new Date(metadata.ttl);
263
}
264
0
0
0
0
0
0
265
// Create stream - will throw if file doesn't exist
266
const stream = createReadStream(filePath);
267
268
return { stream, metadata };
269
} catch (error) {
270
-
const code = (error as NodeJS.ErrnoException).code;
271
-
if (code === 'ENOENT' || code === 'ENOTDIR') {
272
return null;
273
}
274
if (error instanceof SyntaxError) {
···
311
await this.evictIfNeeded(metadata.size);
312
}
313
314
-
// Write metadata first (atomic via temp file)
315
-
const tempMetaPath = `${metaPath}.tmp`;
316
-
await writeFile(tempMetaPath, JSON.stringify(metadata, null, 2));
317
318
// Stream data to file
319
const writeStream = createWriteStream(filePath);
320
await pipeline(stream, writeStream);
321
-
322
-
// Commit metadata
323
-
await rename(tempMetaPath, metaPath);
324
325
this.metadataIndex.set(key, {
326
size: metadata.size,
···
348
await this.evictIfNeeded(data.byteLength);
349
}
350
351
-
const tempMetaPath = `${metaPath}.tmp`;
352
-
await writeFile(tempMetaPath, JSON.stringify(metadata, null, 2));
353
await writeFile(filePath, data);
354
-
await rename(tempMetaPath, metaPath);
355
356
this.metadataIndex.set(key, {
357
size: data.byteLength,
···
379
380
async exists(key: string): Promise<boolean> {
381
const filePath = this.getFilePath(key);
382
-
return existsSync(filePath);
0
0
0
0
0
0
0
0
0
383
}
384
385
async *listKeys(prefix?: string): AsyncIterableIterator<string> {
···
449
450
return metadata;
451
} catch (error) {
452
-
const code = (error as NodeJS.ErrnoException).code;
453
-
if (code === 'ENOENT' || code === 'ENOTDIR') {
454
return null;
455
}
456
if (error instanceof SyntaxError) {
···
469
await mkdir(dir, { recursive: true });
470
}
471
472
-
await writeFile(metaPath, JSON.stringify(metadata, null, 2));
473
}
474
475
async getStats(): Promise<TierStats> {
···
540
} catch {
541
// Directory doesn't exist or can't be read - that's fine
542
return;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
543
}
544
}
545
···
11
} from '../types/index.js';
12
import { encodeKey, decodeKey } from '../utils/path-encoding.js';
13
14
+
function getErrnoCode(error: unknown): string | undefined {
15
+
if (typeof error !== 'object' || error === null) return undefined;
16
+
const maybeCode = (error as { code?: unknown }).code;
17
+
return typeof maybeCode === 'string' ? maybeCode : undefined;
18
+
}
19
+
20
/**
21
* Eviction policy for disk tier when size limit is reached.
22
*/
···
186
const data = await readFile(filePath);
187
return new Uint8Array(data);
188
} catch (error) {
189
+
const code = getErrnoCode(error);
190
+
if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EISDIR') {
191
return null;
192
}
193
throw error;
···
228
229
return { data: new Uint8Array(dataBuffer), metadata };
230
} catch (error) {
231
+
const code = getErrnoCode(error);
232
+
if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EISDIR') {
233
return null;
234
}
235
if (error instanceof SyntaxError) {
···
268
metadata.ttl = new Date(metadata.ttl);
269
}
270
271
+
// Guard against directories being treated as files (causes EISDIR at read time).
272
+
const dataStat = await stat(filePath);
273
+
if (!dataStat.isFile()) {
274
+
return null;
275
+
}
276
+
277
// Create stream - will throw if file doesn't exist
278
const stream = createReadStream(filePath);
279
280
return { stream, metadata };
281
} catch (error) {
282
+
const code = getErrnoCode(error);
283
+
if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EISDIR') {
284
return null;
285
}
286
if (error instanceof SyntaxError) {
···
323
await this.evictIfNeeded(metadata.size);
324
}
325
326
+
// Write metadata first atomically so readers never observe partial JSON.
327
+
await this.writeMetadataAtomically(metaPath, metadata);
0
328
329
// Stream data to file
330
const writeStream = createWriteStream(filePath);
331
await pipeline(stream, writeStream);
0
0
0
332
333
this.metadataIndex.set(key, {
334
size: metadata.size,
···
356
await this.evictIfNeeded(data.byteLength);
357
}
358
359
+
await this.writeMetadataAtomically(metaPath, metadata);
0
360
await writeFile(filePath, data);
0
361
362
this.metadataIndex.set(key, {
363
size: data.byteLength,
···
385
386
async exists(key: string): Promise<boolean> {
387
const filePath = this.getFilePath(key);
388
+
try {
389
+
const fileStat = await stat(filePath);
390
+
return fileStat.isFile();
391
+
} catch (error) {
392
+
const code = getErrnoCode(error);
393
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
394
+
return false;
395
+
}
396
+
throw error;
397
+
}
398
}
399
400
async *listKeys(prefix?: string): AsyncIterableIterator<string> {
···
464
465
return metadata;
466
} catch (error) {
467
+
const code = getErrnoCode(error);
468
+
if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EISDIR') {
469
return null;
470
}
471
if (error instanceof SyntaxError) {
···
484
await mkdir(dir, { recursive: true });
485
}
486
487
+
await this.writeMetadataAtomically(metaPath, metadata);
488
}
489
490
async getStats(): Promise<TierStats> {
···
555
} catch {
556
// Directory doesn't exist or can't be read - that's fine
557
return;
558
+
}
559
+
}
560
+
561
+
/**
562
+
* Atomically write metadata to avoid readers seeing partial JSON.
563
+
*/
564
+
private async writeMetadataAtomically(metaPath: string, metadata: StorageMetadata): Promise<void> {
565
+
const tempMetaPath = `${metaPath}.tmp-${process.pid}-${Date.now()}-${Math.random()
566
+
.toString(16)
567
+
.slice(2)}`;
568
+
try {
569
+
await writeFile(tempMetaPath, JSON.stringify(metadata, null, 2));
570
+
await rename(tempMetaPath, metaPath);
571
+
} catch (error) {
572
+
await unlink(tempMetaPath).catch(() => {});
573
+
throw error;
574
}
575
}
576