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}