Skip to content
File

Blob: samples/static-files-from-disk/static.js

javascript195 lines
1// Copyright (c) 2022-2023 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5// An example Worker that serves static files from disk. This includes logic to do things like
6// set Content-Type based on file extension, look for `index.html` in directories, etc.
7//
8// This code supports several configuration options to control the serving logic, but, better
9// yet, because it's just JavaScript, you can freely edit it to suit your unique needs.
10 
11export default {
12 async fetch(req, env) {
13 if (req.method != "GET" && req.method != "HEAD") {
14 return new Response("Not Implemented", {status: 501});
15 }
16 
17 let url = new URL(req.url);
18 let path = url.pathname;
19 let origPath = path;
20 
21 let config = env.config || {};
22 
23 if (path.endsWith("/") && !config.allowDirectoryListing) {
24 path = path + "index.html";
25 }
26 
27 let content = await env.files.fetch("http://dummy" + path, {method: req.method});
28 
29 if (content.status == 404) {
30 if (config.hideHtmlExtension && !path.endsWith(".html")) {
31 // Try with the `.html` extension.
32 path = path + ".html";
33 content = await env.files.fetch("http://dummy" + path, {method: req.method});
34 }
35 
36 if (!content.ok && config.singlePageApp) {
37 // For any file not found, serve the main page -- NOT as a 404.
38 path = "/index.html";
39 content = await env.files.fetch("http://dummy" + path, {method: req.method});
40 }
41 
42 if (!content.ok) {
43 // None of the fallbacks worked.
44 //
45 // Look for a 404 page.
46 content = await env.files.fetch("http://dummy/404.html", {method: req.method});
47 
48 if (content.ok) {
49 // Return it with a 404 status code.
50 return wrapContent(req, 404, "404.html", content.body, content.headers);
51 } else {
52 // Give up and return generic 404 message.
53 return new Response("404 Not found", {status: 404});
54 }
55 }
56 }
57 
58 if (!content.ok) {
59 // Some error other than 404?
60 console.error("Fetching '" + path + "' returned unexpected status: " + content.status);
61 return new Response("Internal Server Error", {status: 500});
62 }
63 
64 if (content.headers.get("Content-Type") == "application/json") {
65 // This is a directory.
66 if (path.endsWith("/")) {
67 // This must be because `allowDirectoryListing` is `true`, so this is actually OK!
68 let listingHtml = null;
69 if (req.method != "HEAD") {
70 let html = await makeListingHtml(origPath, await content.json(), env.files);
71 return wrapContent(req, 200, "listing.html", html);
72 }
73 } else {
74 // redirect to add '/' suffix.
75 url.pathname = path + "/";
76 return Response.redirect(url);
77 }
78 }
79 
80 if (origPath.endsWith("/index.html")) {
81 // The file exists, but was requested as "index.html", which we want to hide, so redirect
82 // to remove it.
83 url.pathname = origPath.slice(0, -"index.html".length);
84 return Response.redirect(url);
85 }
86 
87 if (config.hideHtmlExtension && origPath.endsWith(".html")) {
88 // The file exists, but was requested with the `.html` extension, which we want to hide, so
89 // redirect to remove it.
90 url.pathname = origPath.slice(0, -".html".length);
91 return Response.redirect(url);
92 }
93 
94 return wrapContent(req, 200, path.split("/").pop(), content.body, content.headers);
95 }
96}
97 
98function wrapContent(req, status, filename, contentBody, contentHeaders) {
99 let type = TYPES[filename.split(".").pop().toLowerCase()] || "application/octet-stream";
100 let headers = { "Content-Type": type };
101 if (type.endsWith(";charset=utf-8")) {
102 let accept = req.headers.get("Accept-Encoding") || "";
103 if (accept.split(",").map(s => s.trim()).includes("gzip")) {
104 // Apply gzip encoding on the fly.
105 // TODO(someday): Support storing gziped copies of files on disk in advance so that gzip
106 // doesn't need to be applied on the fly.
107 headers["Content-Encoding"] = "gzip";
108 }
109 }
110 
111 if (req.method == "HEAD" && contentHeaders) {
112 // Carry over Content-Length header on HEAD requests.
113 let len = contentHeaders.get("Content-Length");
114 if (len) {
115 headers["Content-Length"] = len;
116 }
117 }
118 
119 return new Response(contentBody, {headers, status});
120}
121 
122let TYPES = {
123 txt: "text/plain;charset=utf-8",
124 html: "text/html;charset=utf-8",
125 htm: "text/html;charset=utf-8",
126 css: "text/css;charset=utf-8",
127 js: "text/javascript;charset=utf-8",
128 md: "text/markdown;charset=utf-8",
129 sh: "application/x-shellscript;charset=utf-8",
130 svg: "image/svg+xml;charset=utf-8",
131 xml: "text/xml;charset=utf-8",
132 
133 png: "image/png",
134 jpeg: "image/jpeg",
135 jpg: "image/jpeg",
136 jpe: "image/jpeg",
137 gif: "image/gif",
138 
139 ttf: "font/ttf",
140 woff: "font/woff",
141 woff2: "font/woff2",
142 eot: "application/vnd.ms-fontobject",
143 
144 // When serving files with the .gz extension, we do NOT want to use `Content-Encoding: gzip`,
145 // because this will cause the user agent to unzip it, which is usually not what the user wants
146 // when downloading a gzipped archive.
147 gz: "application/gzip",
148 bz: "application/x-bzip",
149 bz2: "application/x-bzip2",
150 xz: "application/x-xz",
151 zst: "application/zst",
152}
153 
154async function makeListingHtml(path, listing, dir) {
155 if (!path.endsWith("/")) path += "/";
156 
157 let htmlList = [];
158 for (let file of listing) {
159 let len, modified;
160 if (file.type == "file" || file.type == "directory") {
161 let meta = await dir.fetch("http://dummy" + path + file.name, {method: "HEAD"});
162 console.log(meta.status, "http://dummy" + path + file.name, meta.headers.get("Content-Length"));
163 len = meta.headers.get("Content-Length");
164 modified = meta.headers.get("Last-Modified");
165 }
166 
167 len = len || `(${file.type})`;
168 modified = modified || "";
169 
170 htmlList.push(
171 ` <tr>` +
172 `<td><a href="${encodeURIComponent(file.name)}">${file.name}</a></td>` +
173 `<td>${modified}</td><td>${len}</td></tr>`);
174 }
175 
176 return `<!DOCTYPE html>
177<html>
178 <head>
179 <title>Index of ${path}</title>
180 <style type="text/css">
181 td { padding-right: 16px; text-align: right; }
182 td:nth-of-type(1) { font-family: monospace; text-align: left; }
183 th { text-align: left; }
184 </style>
185 </head>
186 <body>
187 <h1>Index of ${path}</h1>
188 <table>
189 <tr><th>Filename</th><th>Modified</th><th>Size</th></tr>
190 ${htmlList.join("\n")}
191 </body>
192</html>
193`
194}