Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

fix extension less file serving

+38 -12
+38 -12
apps/hosting-service/src/lib/file-serving.ts
··· 28 28 type FileStorageResult = StorageResult<Uint8Array>; 29 29 30 30 /** 31 + * Check if the last segment of a path looks like it has a file extension. 32 + * e.g. "style.css" → true, "about" → false, "wisp-cli-x86_64-linux" → false, 33 + * "dir.name/file" → false, "dir/file.tar.gz" → true 34 + */ 35 + function hasFileExtension(path: string): boolean { 36 + const basename = path.split('/').pop() || ''; 37 + return /\.[a-zA-Z0-9]+$/.test(basename); 38 + } 39 + 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 - // For directory-like paths (empty or no extension), try index files FIRST (fast) 316 - // Only do expensive directory listing if needed for directory listing feature 317 - if (!requestPath || !requestPath.includes('.')) { 325 + // For directory-like paths (empty or no file extension in basename), try index files 326 + if (!requestPath || !hasFileExtension(requestPath)) { 327 + // For non-empty extensionless paths, try as a direct file first (e.g. binary downloads) 328 + if (requestPath) { 329 + const directResult = await span(trace, `storage:${requestPath}`, () => getFileWithMetadata(did, rkey, requestPath)); 330 + if (directResult) { 331 + return buildResponseFromStorageResult(directResult, requestPath, settings, requestHeaders); 332 + } 333 + await markExpectedMiss(requestPath); 334 + } 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 - // Not a directory, try to serve as a file 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 - if (!fileRequestPath.includes('.')) { 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 - if (settings?.cleanUrls && !fileRequestPath.includes('.')) { 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 - // For directory-like paths (empty or no extension), try index files FIRST (fast) 619 - // Only do expensive directory listing if needed for directory listing feature 620 - if (!requestPath || !requestPath.includes('.')) { 636 + // For directory-like paths (empty or no file extension in basename), try index files 637 + if (!requestPath || !hasFileExtension(requestPath)) { 638 + // For non-empty extensionless paths, try as a direct file first (e.g. binary downloads) 639 + if (requestPath) { 640 + const directResult = await span(trace, `storage:${requestPath}`, () => getFileForRequest(did, rkey, requestPath, true)); 641 + if (directResult) { 642 + return buildResponseFromStorageResult(directResult.result, requestPath, settings, requestHeaders); 643 + } 644 + await markExpectedMiss(requestPath); 645 + } 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 - // Not a directory, try to serve as a file 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 - if (!fileRequestPath.includes('.')) { 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 - if (settings?.cleanUrls && !fileRequestPath.includes('.')) { 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);