tangled
alpha
login
or
join now
nekomimi.pet
/
wisp.place-monorepo
88
fork
atom
Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
wisp.place
88
fork
atom
overview
issues
9
pulls
pipelines
fix extension less file serving
nekomimi.pet
1 month ago
1939b4d6
1a77307f
1/2
deploy-wisp.yml
success
37s
test.yml
failed
29s
+38
-12
1 changed file
expand all
collapse all
unified
split
apps
hosting-service
src
lib
file-serving.ts
+38
-12
apps/hosting-service/src/lib/file-serving.ts
···
28
28
type FileStorageResult = StorageResult<Uint8Array>;
29
29
30
30
/**
31
31
+
* Check if the last segment of a path looks like it has a file extension.
32
32
+
* e.g. "style.css" → true, "about" → false, "wisp-cli-x86_64-linux" → false,
33
33
+
* "dir.name/file" → false, "dir/file.tar.gz" → true
34
34
+
*/
35
35
+
function hasFileExtension(path: string): boolean {
36
36
+
const basename = path.split('/').pop() || '';
37
37
+
return /\.[a-zA-Z0-9]+$/.test(basename);
38
38
+
}
39
39
+
40
40
+
/**
31
41
* Helper to retrieve a file with metadata from tiered storage
32
42
* Logs which tier the file was served from
33
43
*/
···
312
322
requestPath = requestPath.slice(0, -1);
313
323
}
314
324
315
315
-
// For directory-like paths (empty or no extension), try index files FIRST (fast)
316
316
-
// Only do expensive directory listing if needed for directory listing feature
317
317
-
if (!requestPath || !requestPath.includes('.')) {
325
325
+
// For directory-like paths (empty or no file extension in basename), try index files
326
326
+
if (!requestPath || !hasFileExtension(requestPath)) {
327
327
+
// For non-empty extensionless paths, try as a direct file first (e.g. binary downloads)
328
328
+
if (requestPath) {
329
329
+
const directResult = await span(trace, `storage:${requestPath}`, () => getFileWithMetadata(did, rkey, requestPath));
330
330
+
if (directResult) {
331
331
+
return buildResponseFromStorageResult(directResult, requestPath, settings, requestHeaders);
332
332
+
}
333
333
+
await markExpectedMiss(requestPath);
334
334
+
}
335
335
+
318
336
for (const indexFile of indexFiles) {
319
337
const indexPath = requestPath ? `${requestPath}/${indexFile}` : indexFile;
320
338
const result = await span(trace, `storage:${indexPath}`, () => getFileWithMetadata(did, rkey, indexPath));
···
342
360
// Fall through to normal file serving / 404 handling
343
361
}
344
362
345
345
-
// Not a directory, try to serve as a file
363
363
+
// Try to serve as a file
346
364
const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html';
347
365
348
366
// Retrieve from tiered storage
···
354
372
await markExpectedMiss(fileRequestPath);
355
373
356
374
// Try index files for directory-like paths
357
357
-
if (!fileRequestPath.includes('.')) {
375
375
+
if (!hasFileExtension(fileRequestPath)) {
358
376
for (const indexFileName of indexFiles) {
359
377
const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName;
360
378
const indexResult = await span(trace, `storage:${indexPath}`, () => getFileWithMetadata(did, rkey, indexPath));
···
366
384
}
367
385
368
386
// Try clean URLs: /about -> /about.html
369
369
-
if (settings?.cleanUrls && !fileRequestPath.includes('.')) {
387
387
+
if (settings?.cleanUrls && !hasFileExtension(fileRequestPath)) {
370
388
const htmlPath = `${fileRequestPath}.html`;
371
389
if (await storageExists(did, rkey, htmlPath)) {
372
390
return serveFileInternal(did, rkey, htmlPath, settings, requestHeaders, trace);
···
615
633
requestPath = requestPath.slice(0, -1);
616
634
}
617
635
618
618
-
// For directory-like paths (empty or no extension), try index files FIRST (fast)
619
619
-
// Only do expensive directory listing if needed for directory listing feature
620
620
-
if (!requestPath || !requestPath.includes('.')) {
636
636
+
// For directory-like paths (empty or no file extension in basename), try index files
637
637
+
if (!requestPath || !hasFileExtension(requestPath)) {
638
638
+
// For non-empty extensionless paths, try as a direct file first (e.g. binary downloads)
639
639
+
if (requestPath) {
640
640
+
const directResult = await span(trace, `storage:${requestPath}`, () => getFileForRequest(did, rkey, requestPath, true));
641
641
+
if (directResult) {
642
642
+
return buildResponseFromStorageResult(directResult.result, requestPath, settings, requestHeaders);
643
643
+
}
644
644
+
await markExpectedMiss(requestPath);
645
645
+
}
646
646
+
621
647
for (const indexFile of indexFiles) {
622
648
const indexPath = requestPath ? `${requestPath}/${indexFile}` : indexFile;
623
649
const fileResult = await span(trace, `storage:${indexPath}`, () => getFileForRequest(did, rkey, indexPath, true));
···
645
671
// Fall through to normal file serving / 404 handling
646
672
}
647
673
648
648
-
// Not a directory, try to serve as a file
674
674
+
// Try to serve as a file
649
675
const fileRequestPath: string = requestPath || indexFiles[0] || 'index.html';
650
676
651
677
const fileResult = await span(trace, `storage:${fileRequestPath}`, () => getFileForRequest(did, rkey, fileRequestPath, true));
···
655
681
await markExpectedMiss(fileRequestPath);
656
682
657
683
// Try index files for directory-like paths
658
658
-
if (!fileRequestPath.includes('.')) {
684
684
+
if (!hasFileExtension(fileRequestPath)) {
659
685
for (const indexFileName of indexFiles) {
660
686
const indexPath = fileRequestPath ? `${fileRequestPath}/${indexFileName}` : indexFileName;
661
687
const indexResult = await span(trace, `storage:${indexPath}`, () => getFileForRequest(did, rkey, indexPath, true));
···
667
693
}
668
694
669
695
// Try clean URLs: /about -> /about.html
670
670
-
if (settings?.cleanUrls && !fileRequestPath.includes('.')) {
696
696
+
if (settings?.cleanUrls && !hasFileExtension(fileRequestPath)) {
671
697
const htmlPath = `${fileRequestPath}.html`;
672
698
if (await storageExists(did, rkey, htmlPath)) {
673
699
return serveFileInternalWithRewrite(did, rkey, htmlPath, basePath, settings, requestHeaders, trace);