Streamline mime_type handling so it is kept in the environment and only explicitly...
[jst_server.git] / Site.js
1 let BuildCache = require('BuildCache')
2 let JSONCache = require('JSONCache')
3 let assert = require('assert')
4 let cookie = require('cookie')
5 let emailjs = require('emailjs')
6 let fs = require('fs')
7 let js_template = require('js_template')
8 let less = require('less/lib/less-node')
9 var stream_buffers = require('stream-buffers')
10 let util = require('util')
11 let url = require('url')
12 let yauzl = require('yauzl')
13 let zetjs = require('zetjs')
14
15 let fs_mkdir = util.promisify(fs.mkdir)
16 let fs_readFile = util.promisify(fs.readFile)
17 let fs_stat = util.promisify(fs.stat)
18 let yauzl_open = util.promisify(yauzl.open)
19
20 let Site = function(server, root) {
21   if (!this instanceof Site)
22     throw Error('Site is a constructor')
23   this.server = server
24   this.root = root
25   this.socket_io_connect_listeners = []
26 }
27
28 Site.prototype.get_email = function(path) {
29   path = this.root + path
30   return this.server.build_cache_email.get(
31     path,
32     async result => {
33       console.log('getting', path, 'as email')
34       result.value = emailjs.this.server.connect(
35         JSON.parse(await fs_readFile(path))
36       )
37     }
38   )
39 }
40
41 // this is for read-only JSON files
42 // they will be reloaded from disk if modified
43 Site.prototype.get_json = function(path) {
44   path = this.root + path
45   return this.server.build_cache_json.get(
46     path,
47     async result => {
48       console.log('getting', path, 'as json')
49       result.value = JSON.parse(await fs_readFile(path))
50     }
51   )
52 }
53
54 Site.prototype.get_less = function(dirname, path) {
55   path = this.root + path
56   return this.server.build_cache_less.get(
57     path,
58     async result => {
59       console.log('getting', path, 'as less')
60       let render = await less.render(
61         await fs_readFile(path, {encoding: 'utf-8'}),
62         {
63           //color: true,
64           //compress: false,
65           //depends: false,
66           filename: path,
67           //globalVars: null,
68           //ieCompat: false,
69           //insecure: false,
70           //javascriptEnabled: false,
71           //lint: false,
72           //math: 0,
73           //modifyVars: null,
74           paths: [this.root + dirname],
75           //plugins: [],
76           //reUsePluginManager: true,
77           //rewriteUrls: false,
78           rootpath: this.root//,
79           //strictImports: false,
80           //strictUnits: false,
81           //urlArgs: ''
82         }
83       )
84       result.deps.concat(render.imports)
85       result.value = Buffer.from(render.css)
86     }
87   )
88 }
89
90 Site.prototype.get_text = function(path) {
91   path = this.root + path
92   return this.server.build_cache_text.get(
93     path,
94     async result => {
95       console.log('getting', path, 'as text')
96       result.value = await fs_readFile(path, {encoding: 'utf-8'})
97     }
98   )
99 }
100
101 Site.prototype.get_zet = function(path) {
102   path = this.root + path
103   return this.server.build_cache_zet.get(
104     path,
105     async result => {
106       console.log('getting', path, 'as zet')
107       result.deps = [
108         path + '.map.0',
109         path + '.param.0',
110         path + '.v.0',
111         path + '.vocab.0'
112       ]
113       result.value = new zetjs.Index(path)
114     }
115   )
116 }
117
118 Site.prototype.get_zip = function(path) {
119   path = this.root + path
120   return this.server.build_cache_zip.get(
121     path,
122     async result => {
123       console.log('getting', path, 'as zip')
124       result.value = {}
125       let zipfile = await yauzl_open(path, {autoClose: false})
126       let entries = []
127       await new Promise(
128         (resolve, reject) => {
129           zipfile.
130           on('entry', entry => {entries.push(entry)}).
131           on('end', () => resolve())
132         }
133       )
134       for (let i = 0; i < entries.length; ++i) {
135         let read_stream = await new Promise(
136           (resolve, reject) => {
137             zipfile.openReadStream(
138               entries[i],
139               (err, stream) => {
140                 if (err)
141                   reject(err)
142                 resolve(stream)
143               }
144             )
145           }
146         )
147         let write_stream = new stream_buffers.WritableStreamBuffer()
148         let data = new Promise(
149           (resolve, reject) => {
150             write_stream.
151             on('finish', () => {resolve(write_stream.getContents())}).
152             on('error', () => {reject()})
153           }
154         )
155         read_stream.pipe(write_stream)
156         let path = '/' + entries[i].fileName
157         data = await data
158         console.log('entry path', path, 'size', data.length)
159         result.value[path] = data
160       }
161       await zipfile.close()
162     }
163   )
164 }
165
166 Site.prototype.ensure_dir = async function(path) {
167   try {
168     await fs_mkdir(this.root + path)
169   }
170   catch (err) {
171     if (err.code !== 'EEXIST') // should check error type
172       throw err
173   }
174 }
175
176 // this is for read/write JSON files
177 // they will not be reloaded from disk if modified
178 Site.prototype.open_json = async function(path, default_value) {
179   return /*await*/ this.server.json_cache.open(this.root + path, default_value)
180 }
181 Site.prototype.flush_json = async function(path) {
182   return /*await*/ this.server.json_cache.flush(this.root + path)
183 }
184
185 Site.prototype.serve_jst = async function(env, pathname) {
186   let jst
187   try {
188     jst = await js_template(this.root, this.root, this.root + pathname)
189   }
190   catch (err) {
191     if (err.code !== 'ENOENT')
192       throw err
193     return false
194   }
195   let out = []
196   await jst(env, out)
197   let data = Buffer.from(out.join(''))
198   console.log(
199     `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from jst`
200   )
201   this.server.serve(env.response, 200, env.mime_type, data)
202   return true
203 }
204
205 Site.prototype.serve_less = async function(env, pathname) {
206   if (env.pathname.slice(env.pathname_pos) !== '.css')
207     return false
208  
209   let data 
210   try {
211     data = await this.get_less(this.root, pathname)
212   }
213   catch (err) {
214     if (err.code !== 'ENOENT')
215       throw err
216     return false
217   }
218   console.log(
219     `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from less`
220   )
221   this.server.serve(env.response, 200, env.mime_type, data)
222   return true
223 }
224
225 Site.prototype.serve_fs = async function(env, pathname) {
226   let data 
227   try {
228     data = await fs_readFile(this.root + pathname)
229   }
230   catch (err) {
231     if (err.code !== 'ENOENT')
232       throw err
233     return false
234   }
235   console.log(
236     `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from fs`
237   )
238   this.server.serve(env.response, 200, env.mime_type, data)
239   return true
240 }
241
242
243 Site.prototype.serve_zip = async function(env, pathname) {
244   let zip 
245   try {
246     zip = await this.get_zip(pathname)
247   }
248   catch (err) {
249     if (err.code !== 'ENOENT')
250       throw err
251     return false
252   }
253   if (!Object.prototype.hasOwnProperty.call(zip, env.pathname))
254     return false
255   let data = zip[env.pathname]
256   console.log(
257     `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from zip`
258   )
259   this.server.serve(env.response, 200, env.mime_type, data)
260   return true
261 }
262
263 Site.prototype.respond = async function(env) {
264   while (true) {
265     if (env.pathname_pos >= env.pathname.length) {
266       let pathname = env.pathname + '/index.html'
267       console.log(
268         `${env.parsed_url.host} redirecting ${env.pathname} to ${pathname}`
269       )
270       this.server.redirect(
271         env.response,
272         pathname + (env.parsed_url.search || '')
273       )
274       return
275     }
276
277     assert(env.pathname.charAt(env.pathname_pos) === '/')
278     let i = env.pathname_pos + 1
279     let j = env.pathname.indexOf('/', i)
280     if (j === -1)
281       j = env.pathname.length
282     let filename = env.pathname.slice(i, j)
283
284     if (filename.length === 0) {
285       if (j >= env.pathname.length) {
286         let pathname = env.pathname + 'index.html'
287         console.log(
288           `${env.parsed_url.host} redirecting ${env.pathname} to ${pathname}`
289         )
290         this.server.redirect(
291           env.response,
292           pathname + (env.parsed_url.search || '')
293         )
294       }
295       else {
296         console.log(
297           `${env.parsed_url.host} empty directory name in ${env.pathname}`
298         )
299         this.server.die(env.response)
300       }
301       return
302     }
303
304     if (
305       filename.charAt(0) === '.' ||
306       filename.charAt(0) === '_'
307     ) {
308       console.log(
309         `${env.parsed_url.host} bad component "${filename}" in ${env.pathname}`
310       )
311       this.server.die(env.response)
312       return
313     }
314
315     let k = filename.lastIndexOf('.')
316     if (k === -1)
317       k = filename.length
318     let filetype = filename.slice(k)
319
320     if (
321       filetype.length !== 0 &&
322       Object.prototype.hasOwnProperty.call(this.server.mime_types, filetype)
323     ) {
324       if (j < env.pathname.length) {
325         console.log(
326           `${env.parsed_url.host} non-directory filetype "${filetype}" in ${env.pathname}`
327         )
328         this.server.die(env.response)
329         return
330       }
331       env.mime_type = this.server.mime_types[filetype]
332       env.pathname_pos = i + k // advance to "." at start of filetype
333       break
334     }
335
336     env.pathname_pos = j
337     let pathname = env.pathname.slice(0, env.pathname_pos)
338     if (await this.serve_jst(env, pathname + '.dir.jst'))
339       return
340
341     let stats
342     try {
343       stats = await fs_stat(this.root + pathname)
344     }
345     catch (err) {
346       if (err.code !== 'ENOENT')
347         throw err
348       console.log(
349         `${env.parsed_url.host} directory not found: ${pathname}`
350       )
351       this.server.die(env.response)
352       return
353     }
354     if (!stats.isDirectory()) {
355       console.log(
356         `${env.parsed_url.host} not directory: ${pathname}`
357       )
358       this.server.die(env.response)
359       return
360     }
361   }    
362
363   if (
364     !await this.serve_jst(env, env.pathname + '.jst') &&
365     !await this.serve_less(env, env.pathname + '.less') &&
366     !await this.serve_fs(env, env.pathname) &&
367     !await this.serve_zip(env, '/favicons.zip')
368   ) {
369     console.log(
370       `${env.parsed_url.host} file not found ${env.pathname}`
371     )
372     this.server.die(env.response)
373   }
374 }
375
376 module.exports = Site