| 1 | module file |
| 2 | |
| 3 | import os |
| 4 | import log |
| 5 | import time |
| 6 | import runtime |
| 7 | import net.http |
| 8 | import net.http.mime |
| 9 | import net.urllib |
| 10 | |
| 11 | @[params] |
| 12 | pub struct StaticServeParams { |
| 13 | pub mut: |
| 14 | folder string = $d('http_folder', '.') // The folder, that will be used as a base for serving all static resources; If it was /tmp, then: http://localhost:4001/x.txt => /tmp/x.txt . Customize with `-d http_folder=vlib/_docs`. |
| 15 | index_file string = $d('http_index_file', 'index.html') // A request for http://localhost:4001/ will map to `index.html`, if that file is present. |
| 16 | auto_index bool = $d('http_auto_index', true) // when an index_file is *not* present, a request for http://localhost:4001/ will list automatically all files in the folder. |
| 17 | on string = $d('http_on', 'localhost:4001') // on which address:port to listen for http requests. |
| 18 | filter_myexe bool = true // whether to filter the name of the static file executable from the automatic folder listings for / . Useful with `v -e 'import net.http.file; file.serve()'` |
| 19 | workers int = runtime.nr_jobs() // how many worker threads to use for serving the responses, by default it is limited to the number of available cores; can be controlled with setting VJOBS |
| 20 | shutdown_after time.Duration = time.infinite // after this time has passed, the webserver will gracefully shutdown on its own |
| 21 | } |
| 22 | |
| 23 | // serve will start a static files web server. |
| 24 | // |
| 25 | // The most common usage is the following: `v -e 'import net.http.file; file.serve()'` |
| 26 | // will listen for http requests on port 4001 by default, and serve all the files in the current folder. |
| 27 | // |
| 28 | // Another example: `v -e 'import net.http.file; file.serve(folder: "/tmp")'` |
| 29 | // will serve all files inside the /tmp folder. |
| 30 | // |
| 31 | // Another example: `v -e 'import net.http.file; file.serve(folder: "~/Projects", on: ":5002")'` |
| 32 | // will expose all the files inside the ~/Projects folder, on http://localhost:5002/ . |
| 33 | pub fn serve(params StaticServeParams) { |
| 34 | mut nparams := params |
| 35 | nparams.folder = os.norm_path(os.real_path(params.folder)) |
| 36 | mut server := &http.Server{ |
| 37 | handler: StaticHttpHandler{ |
| 38 | params: nparams |
| 39 | } |
| 40 | addr: params.on |
| 41 | worker_num: params.workers |
| 42 | } |
| 43 | if params.shutdown_after != time.infinite { |
| 44 | spawn fn (params StaticServeParams, mut server http.Server) { |
| 45 | log.warn('This file server, will shutdown itself after ${params.shutdown_after}.') |
| 46 | time.sleep(params.shutdown_after) |
| 47 | log.warn('Graceful shutdown, because the file server started ${params.shutdown_after} ago.') |
| 48 | server.stop() |
| 49 | }(params, mut server) |
| 50 | } |
| 51 | log.warn('${@METHOD}, starting...') |
| 52 | server.listen_and_serve() |
| 53 | log.warn('${@METHOD}, done.') |
| 54 | } |
| 55 | |
| 56 | // implementation details: |
| 57 | |
| 58 | struct StaticHttpHandler { |
| 59 | params StaticServeParams |
| 60 | } |
| 61 | |
| 62 | const no_such_file_doc = '<!DOCTYPE html><h1>no such file</h1>' |
| 63 | |
| 64 | fn (mut h StaticHttpHandler) handle(req http.Request) http.Response { |
| 65 | mut res := http.new_response(body: '') |
| 66 | sw := time.new_stopwatch() |
| 67 | mut url := urllib.query_unescape(req.url) or { |
| 68 | log.warn('bad request; url: ${req.url} ') |
| 69 | res.set_status(.bad_request) |
| 70 | res.body = '<!DOCTYPE html><h1>url decode fail</h1>' |
| 71 | res.header.add(.content_type, 'text/html; charset=utf-8') |
| 72 | return res |
| 73 | } |
| 74 | defer { |
| 75 | log.info('took: ${sw.elapsed().microseconds():6}µs, status: ${res.status_code}, size: ${res.body.len:9}, url: ${url}') |
| 76 | } |
| 77 | mut uri_path := url.all_after_first('/').all_before('?').trim_right('/') |
| 78 | requested_file_path := |
| 79 | os.norm_path(os.real_path(os.join_path_single(h.params.folder, uri_path))) |
| 80 | if !requested_file_path.starts_with(h.params.folder) { |
| 81 | log.warn('forbidden request; base folder: ${h.params.folder}, requested_file_path: ${requested_file_path}, ') |
| 82 | res.set_status(.forbidden) |
| 83 | res.body = '<h1>forbidden</h1>' |
| 84 | res.header.add(.content_type, 'text/html; charset=utf-8') |
| 85 | return res |
| 86 | } |
| 87 | if !os.exists(requested_file_path) { |
| 88 | res.set_status(.not_found) |
| 89 | res.body = no_such_file_doc |
| 90 | res.header.add(.content_type, 'text/html; charset=utf-8') |
| 91 | return res |
| 92 | } |
| 93 | |
| 94 | mut body := '' |
| 95 | mut content_type := 'text/html; charset=utf-8' |
| 96 | if os.is_dir(requested_file_path) { |
| 97 | ipath := os.join_path_single(requested_file_path, h.params.index_file) |
| 98 | if h.params.auto_index { |
| 99 | if h.params.index_file == '' { |
| 100 | body = get_folder_index_html(requested_file_path, uri_path, h.params.filter_myexe) |
| 101 | } else { |
| 102 | body = os.read_file(ipath) or { |
| 103 | get_folder_index_html(requested_file_path, uri_path, h.params.filter_myexe) |
| 104 | } |
| 105 | } |
| 106 | } else { |
| 107 | body = os.read_file(ipath) or { |
| 108 | res.set_status(.not_found) |
| 109 | no_such_file_doc |
| 110 | } |
| 111 | } |
| 112 | } else { |
| 113 | body = os.read_file(requested_file_path) or { |
| 114 | res.set_status(.not_found) |
| 115 | 'not found' |
| 116 | } |
| 117 | mt := mime.get_mime_type(os.file_ext(requested_file_path).all_after_first('.')) |
| 118 | content_type = mime.get_content_type(mt) |
| 119 | } |
| 120 | res.body = body |
| 121 | res.header.add(.content_type, content_type) |
| 122 | return res |
| 123 | } |
| 124 | |