Static site hosting via tangled
1import { 2 getContentTypeForFilename, 3 trimLeadingSlash, 4 extname, 5 joinurl, 6} from "./helpers.js"; 7import { KnotClient } from "./knot-client.js"; 8 9class FileCache { 10 constructor() { 11 this.cache = new Map(); 12 } 13 14 get(filename) { 15 return this.cache.get(filename) ?? null; 16 } 17 18 set(filename, content) { 19 this.cache.set(filename, content); 20 } 21 22 clear() { 23 this.cache.clear(); 24 } 25} 26 27export class PagesService { 28 constructor({ 29 knotDomain, 30 ownerDid, 31 repoName, 32 branch = "main", 33 baseDir = "/", 34 notFoundFilepath = null, 35 cache, 36 }) { 37 this.knotDomain = knotDomain; 38 this.ownerDid = ownerDid; 39 this.repoName = repoName; 40 this.branch = branch; 41 this.baseDir = baseDir; 42 this.notFoundFilepath = notFoundFilepath; 43 this.client = new KnotClient({ 44 domain: knotDomain, 45 ownerDid, 46 repoName, 47 branch, 48 }); 49 this.fileCache = null; 50 if (cache) { 51 console.log("Enabling cache for", this.ownerDid, this.repoName); 52 this.fileCache = new FileCache(); 53 } 54 } 55 56 async getFileContent(filename) { 57 const cachedContent = this.fileCache?.get(filename); 58 if (cachedContent) { 59 console.log("Cache hit for", filename); 60 return cachedContent; 61 } 62 let content = null; 63 const blob = await this.client.getBlob(filename); 64 if (blob.is_binary) { 65 content = await this.client.getRaw(filename); 66 } else { 67 content = blob.contents; 68 } 69 this.fileCache?.set(filename, content); 70 return content; 71 } 72 73 async getPage(route) { 74 let filePath = route; 75 const extension = extname(filePath); 76 if (!extension) { 77 filePath = joinurl(filePath, "index.html"); 78 } 79 const fullPath = joinurl(this.baseDir, trimLeadingSlash(filePath)); 80 const content = await this.getFileContent(fullPath); 81 if (!content) { 82 return this.get404(); 83 } 84 return { 85 status: 200, 86 content, 87 contentType: getContentTypeForFilename(fullPath), 88 }; 89 } 90 91 async get404() { 92 if (this.notFoundFilepath) { 93 const fullPath = joinurl( 94 this.baseDir, 95 trimLeadingSlash(this.notFoundFilepath) 96 ); 97 const content = await this.getFileContent(fullPath); 98 if (!content) { 99 console.warn("'Not found' file not found", fullPath); 100 return { status: 404, content: "Not Found", contentType: "text/plain" }; 101 } 102 return { 103 status: 404, 104 content, 105 contentType: getContentTypeForFilename(this.notFoundFilepath), 106 }; 107 } 108 return { status: 404, content: "Not Found", contentType: "text/plain" }; 109 } 110 111 async clearCache() { 112 if (!this.fileCache) { 113 console.log("No cache to clear for", this.ownerDid, this.repoName); 114 return; 115 } 116 console.log("Clearing cache for", this.ownerDid, this.repoName); 117 this.fileCache.clear(); 118 } 119}