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