12f182d0f2eb636f4ce11ecb827e55b4ee662ecc
[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   let mime_type = await jst(env, out)
197   if (mime_type === undefined) {
198     // for directories the mime type must be returned, for files we
199     // can look it up from the pathname starting at current position
200     // (for files we're guaranteed to be on last pathname component)
201     let filetype = env.pathname.slice(env.pathname_pos) 
202     assert(
203       Object.prototype.hasOwnProperty.call(this.server.mime_types, filetype)
204     )
205     mime_type = this.server.mime_types[filetype]
206   }
207   let data = Buffer.from(out.join(''))
208   console.log(
209     `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from jst`
210   )
211   this.server.serve(env.response, 200, mime_type, data)
212   return true
213 }
214
215 Site.prototype.serve_less = async function(env, pathname) {
216   if (env.pathname.slice(env.pathname_pos) !== '.css')
217     return false
218  
219   let data 
220   try {
221     data = await this.get_less(this.root, pathname)
222   }
223   catch (err) {
224     if (err.code !== 'ENOENT')
225       throw err
226     return false
227   }
228   console.log(
229     `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from less`
230   )
231   this.server.serve(env.response, 200, this.server.mime_types['.css'], data)
232   return true
233 }
234
235 Site.prototype.serve_fs = async function(env, pathname) {
236   let data 
237   try {
238     data = await fs_readFile(this.root + pathname)
239   }
240   catch (err) {
241     if (err.code !== 'ENOENT')
242       throw err
243     return false
244   }
245   console.log(
246     `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from fs`
247   )
248   let filetype = env.pathname.slice(env.pathname_pos)
249   this.server.serve(env.response, 200, this.server.mime_types[filetype], data)
250   return true
251 }
252
253
254 Site.prototype.serve_zip = async function(env, pathname) {
255   let zip 
256   try {
257     zip = await this.get_zip(pathname)
258   }
259   catch (err) {
260     if (err.code !== 'ENOENT')
261       throw err
262     return false
263   }
264   if (!Object.prototype.hasOwnProperty.call(zip, env.pathname))
265     return false
266   let data = zip[env.pathname]
267   console.log(
268     `${env.parsed_url.host} serving ${env.pathname} length ${data.length} from zip`
269   )
270   let filetype = env.pathname.slice(env.pathname_pos)
271   this.server.serve(env.response, 200, this.server.mime_types[filetype], data)
272   return true
273 }
274
275 Site.prototype.respond = async function(env) {
276   while (true) {
277     if (env.pathname_pos >= env.pathname.length) {
278       let pathname = env.pathname + '/index.html'
279       console.log(
280         `${env.parsed_url.host} redirecting ${env.pathname} to ${pathname}`
281       )
282       this.server.redirect(
283         env.response,
284         pathname + (env.parsed_url.search || '')
285       )
286       return
287     }
288
289     assert(env.pathname.charAt(env.pathname_pos) === '/')
290     let i = env.pathname_pos + 1
291     let j = env.pathname.indexOf('/', i)
292     if (j === -1)
293       j = env.pathname.length
294     let filename = env.pathname.slice(i, j)
295
296     if (filename.length === 0) {
297       if (j >= env.pathname.length) {
298         let pathname = env.pathname + 'index.html'
299         console.log(
300           `${env.parsed_url.host} redirecting ${env.pathname} to ${pathname}`
301         )
302         this.server.redirect(
303           env.response,
304           pathname + (env.parsed_url.search || '')
305         )
306       }
307       else {
308         console.log(
309           `${env.parsed_url.host} empty directory name in ${env.pathname}`
310         )
311         this.server.die(env.response)
312       }
313       return
314     }
315
316     if (
317       filename.charAt(0) === '.' ||
318       filename.charAt(0) === '_'
319     ) {
320       console.log(
321         `${env.parsed_url.host} bad component "${filename}" in ${env.pathname}`
322       )
323       this.server.die(env.response)
324       return
325     }
326
327     let k = filename.lastIndexOf('.')
328     if (k === -1)
329       k = filename.length
330     let filetype = filename.slice(k)
331
332     if (
333       filetype.length !== 0 &&
334       Object.prototype.hasOwnProperty.call(this.server.mime_types, filetype)
335     ) {
336       if (j < env.pathname.length) {
337         console.log(
338           `${env.parsed_url.host} non-directory filetype "${filetype}" in ${env.pathname}`
339         )
340         this.server.die(env.response)
341         return
342       }
343       env.pathname_pos = i + k // advance to "." at start of filetype
344       break
345     }
346
347     env.pathname_pos = j
348     let pathname = env.pathname.slice(0, env.pathname_pos)
349     if (await this.serve_jst(env, pathname + '.dir.jst'))
350       return
351
352     let stats
353     try {
354       stats = await fs_stat(this.root + pathname)
355     }
356     catch (err) {
357       if (err.code !== 'ENOENT')
358         throw err
359       console.log(
360         `${env.parsed_url.host} directory not found: ${pathname}`
361       )
362       this.server.die(env.response)
363       return
364     }
365     if (!stats.isDirectory()) {
366       console.log(
367         `${env.parsed_url.host} not directory: ${pathname}`
368       )
369       this.server.die(env.response)
370       return
371     }
372   }    
373
374   if (
375     !await this.serve_jst(env, env.pathname + '.jst') &&
376     !await this.serve_less(env, env.pathname + '.less') &&
377     !await this.serve_fs(env, env.pathname) &&
378     !await this.serve_zip(env, '/favicons.zip')
379   ) {
380     console.log(
381       `${env.parsed_url.host} file not found ${env.pathname}`
382     )
383     this.server.die(env.response)
384   }
385 }
386
387 module.exports = Site