mirror of
https://github.com/vlang/v.git
synced 2023-08-10 21:13:21 +03:00
docs: improve vlib/vweb/README.md (#15146)
This commit is contained in:
parent
a1de8f5f98
commit
eafbf335cf
@ -1,78 +1,140 @@
|
||||
# vweb - the V Web Server #
|
||||
# vweb - the V Web Server
|
||||
|
||||
A simple yet powerful web server with built-in routing, parameter handling,
|
||||
templating, and other features.
|
||||
A simple yet powerful web server with built-in routing, parameter handling, templating, and other
|
||||
features.
|
||||
The [gitly](https://gitly.org/) site is based on vweb.
|
||||
|
||||
## Alpha level software ##
|
||||
**_Some features may not be complete, and have some bugs._**
|
||||
|
||||
Some features may not be complete, and there may still be bugs. However, it is
|
||||
still a very useful tool. The [gitly](https://gitly.org/) site is based on vweb.
|
||||
|
||||
## Features ##
|
||||
## Features
|
||||
|
||||
- **Very fast** performance of C on the web.
|
||||
- **Small binary** hello world website is <100 KB.
|
||||
- **Easy to deploy** just one binary file that also includes all templates.
|
||||
No need to install any dependencies.
|
||||
- **Templates are precompiled** all errors are visible at compilation time,
|
||||
not at runtime.
|
||||
- **Easy to deploy** just one binary file that also includes all templates. No need to install any
|
||||
dependencies.
|
||||
- **Templates are precompiled** all errors are visible at compilation time, not at runtime.
|
||||
|
||||
There is no formal documentation yet - here is a simple
|
||||
[example](https://github.com/vlang/v/tree/master/examples/vweb/vweb_example.v)
|
||||
### Examples
|
||||
|
||||
There's also the V forum, [vorum](https://github.com/vlang/vorum)
|
||||
There are some examples
|
||||
that can be explored [here](https://github.com/vlang/v/tree/master/examples/vweb).
|
||||
|
||||
`vorum.v` contains all GET and POST actions.
|
||||
And others like:
|
||||
|
||||
- [vweb_orm_jwt](https://github.com/vlang/v/tree/master/examples/vweb_orm_jwt) (back-end)
|
||||
- [vorum](https://github.com/vlang/vorum) (front-end)
|
||||
- [gitly](https://github.com/vlang/gitly) (full-stack)
|
||||
|
||||
**Front-end getting start example**
|
||||
`src/main.v`
|
||||
|
||||
```v ignore
|
||||
pub fn (app mut App) index() {
|
||||
posts := app.find_all_posts()
|
||||
$vweb.html()
|
||||
module main
|
||||
|
||||
import vweb
|
||||
import os
|
||||
|
||||
struct App {
|
||||
vweb.Context
|
||||
}
|
||||
|
||||
// TODO ['/post/:id/:title']
|
||||
// TODO `fn (app App) post(id int)`
|
||||
pub fn (app App) post() {
|
||||
id := app.get_post_id()
|
||||
post := app.retrieve_post(id) or {
|
||||
app.redirect('/')
|
||||
return
|
||||
}
|
||||
comments := app.find_comments(id)
|
||||
show_form := true
|
||||
$vweb.html()
|
||||
struct Object {
|
||||
title string
|
||||
description string
|
||||
}
|
||||
|
||||
fn main() {
|
||||
vweb.run_at(new_app(), vweb.RunParams{
|
||||
port: 8081
|
||||
}) or { panic(err) }
|
||||
}
|
||||
|
||||
fn new_app() &App {
|
||||
mut app := &App{}
|
||||
// makes all static files available.
|
||||
app.mount_static_folder_at(os.resource_abs_path('.'), '/')
|
||||
return app
|
||||
}
|
||||
|
||||
['/']
|
||||
pub fn (mut app App) page_home() vweb.Result {
|
||||
// all this constants can be accessed by src/templates/page/home.html file.
|
||||
page_title := 'V is the new V'
|
||||
v_url := 'https://github.com/vlang/v'
|
||||
|
||||
list_of_object := [
|
||||
Object{
|
||||
title: 'One good title'
|
||||
description: 'this is the first'
|
||||
},
|
||||
Object{
|
||||
title: 'Other good title'
|
||||
description: 'more one'
|
||||
},
|
||||
]
|
||||
// $vweb.html() in `<folder>_<name> vweb.Result ()` like this
|
||||
// render the `<name>.html` in folder `./templates/<folder>`
|
||||
return $vweb.html()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`index.html` is an example of the V template language:
|
||||
|
||||
```html
|
||||
@for post in posts
|
||||
<div class=post>
|
||||
<a class=topic href="@post.url">@post.title</a>
|
||||
<img class=comment-img>
|
||||
<span class=nr-comments>@post.nr_comments</span>
|
||||
<span class=time>@post.time</span>
|
||||
</div>
|
||||
@end
|
||||
```
|
||||
|
||||
`$vweb.html()` compiles an HTML template into V during compilation,
|
||||
and embeds the resulting code into the current action.
|
||||
`$vweb.html()` compiles an HTML template into V during compilation, and embeds the resulting code
|
||||
into the current action.
|
||||
|
||||
That means that the template automatically has access to that action's entire environment.
|
||||
|
||||
## Deploying vweb apps ##
|
||||
`src/templates/page/home.html`
|
||||
|
||||
```html
|
||||
<html>
|
||||
<header>
|
||||
<title>${page_title}</title>
|
||||
@css 'src/templates/page/home.css'
|
||||
</header>
|
||||
<body>
|
||||
<h1 class="title">Hello, Vs.</h1>
|
||||
@for var in list_of_object
|
||||
<div>
|
||||
<a href="${v_url}">${var.title}</a>
|
||||
<span>${var.description}</span>
|
||||
</div>
|
||||
@end
|
||||
<div>@include 'component.html'</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
`src/templates/page/component.html`
|
||||
|
||||
```html
|
||||
<div>This is a component</div>
|
||||
```
|
||||
|
||||
`src/templates/page/home.css`
|
||||
|
||||
```css
|
||||
h1.title {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #3b7bbf;
|
||||
}
|
||||
```
|
||||
|
||||
V suport some [Template directives](/vlib/v/TEMPLATES.md) like
|
||||
`@css`, `@js` for static files in \<path\>
|
||||
`@if`, `@for` for conditional and loop
|
||||
and
|
||||
`@include` to include html components.
|
||||
|
||||
## Deploying vweb apps
|
||||
|
||||
Everything, including HTML templates, is in one binary file. That's all you need to deploy.
|
||||
|
||||
## Getting Started ##
|
||||
## Getting Started
|
||||
|
||||
To start with vweb, you have to import the module `vweb`. After the import,
|
||||
define a struct to hold vweb.Context (and any other variables your program will
|
||||
need).
|
||||
|
||||
The web server can be started by calling `vweb.run(&App{}, port)`.
|
||||
To start with vweb, you have to import the module `vweb` and define a struct to hold vweb.Context
|
||||
(and any other variables your program will need).
|
||||
The web server can be started by calling `vweb.run(&App{}, port)` or `vweb.run(&App{}, RunParams)`
|
||||
|
||||
**Example:**
|
||||
|
||||
@ -85,10 +147,16 @@ struct App {
|
||||
|
||||
fn main() {
|
||||
vweb.run(&App{}, 8080)
|
||||
// // or
|
||||
// vweb.run_at(new_app(), vweb.RunParams{
|
||||
// host: 'localhost'
|
||||
// port: 8099
|
||||
// family: .ip
|
||||
// }) or { panic(err) }
|
||||
}
|
||||
```
|
||||
|
||||
### Defining endpoints ###
|
||||
### Defining endpoints
|
||||
|
||||
To add endpoints to your web server, you have to extend the `App` struct.
|
||||
For routing you can either use auto-mapping of function names or specify the path as an attribute.
|
||||
@ -109,7 +177,11 @@ fn (mut app App) world() vweb.Result {
|
||||
}
|
||||
```
|
||||
|
||||
To create an HTTP POST endpoint, you simply add a `[post]` attribute before the function definition.
|
||||
#### - HTTP verbs
|
||||
|
||||
To use any HTTP verbs (or methods, as they are properly called),
|
||||
such as `[post]`, `[get]`, `[put]`, `[patch]` or `[delete]`
|
||||
you can simply add the attribute before the function definition.
|
||||
|
||||
**Example:**
|
||||
|
||||
@ -118,16 +190,26 @@ To create an HTTP POST endpoint, you simply add a `[post]` attribute before the
|
||||
fn (mut app App) world() vweb.Result {
|
||||
return app.text('World')
|
||||
}
|
||||
|
||||
['/product/create'; post]
|
||||
fn (mut app App) create_product() vweb.Result {
|
||||
return app.text('product')
|
||||
}
|
||||
```
|
||||
|
||||
To pass a parameter to an endpoint, you simply define it inside
|
||||
an attribute, e. g. `['/hello/:user]`.
|
||||
#### - Parameters
|
||||
|
||||
Parameters are passed direcly in endpoint route using colon sign `:` and received using the same
|
||||
name at function
|
||||
To pass a parameter to an endpoint, you simply define it inside an attribute, e. g.
|
||||
`['/hello/:user]`.
|
||||
After it is defined in the attribute, you have to add it as a function parameter.
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
['/hello/:user']
|
||||
vvvv
|
||||
['/hello/:user'] vvvv
|
||||
fn (mut app App) hello_user(user string) vweb.Result {
|
||||
return app.text('Hello $user')
|
||||
}
|
||||
@ -139,3 +221,343 @@ If you want to read the request body, you can do that by calling `app.req.data`.
|
||||
To read the request headers, you just call `app.req.header` and access the
|
||||
header you want, e.g. `app.req.header.get(.content_type)`. See `struct Header`
|
||||
for all available methods (`v doc net.http Header`).
|
||||
|
||||
### Middleware
|
||||
|
||||
V haven't a well defined middleware.
|
||||
For now, you can use `before_request()`. This method called before every request.
|
||||
Probably you can use it for check user session cookie or add header
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
pub fn (mut app App) before_request() {
|
||||
app.user_id = app.get_cookie('id') or { '0' }
|
||||
}
|
||||
```
|
||||
|
||||
### Redirect
|
||||
|
||||
Used when you want be redirected to an url
|
||||
**Examples:**
|
||||
|
||||
```v ignore
|
||||
pub fn (mut app App) before_request() {
|
||||
app.user_id = app.get_cookie('id') or { app.redirect('/') }
|
||||
}
|
||||
```
|
||||
|
||||
```v ignore
|
||||
['/articles'; get]
|
||||
pub fn (mut app App) articles() vweb.Result {
|
||||
if !app.token {
|
||||
app.redirect('/login')
|
||||
}
|
||||
return app.text("patatoes")
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
#### - set_status
|
||||
|
||||
Sets the response status
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
['/user/get_all'; get]
|
||||
pub fn (mut app App) controller_get_all_user() vweb.Result {
|
||||
token := app.get_header('token')
|
||||
|
||||
if !token {
|
||||
app.set_status(401, '')
|
||||
return app.text('Not valid token')
|
||||
}
|
||||
|
||||
response := app.service_get_all_user() or {
|
||||
app.set_status(400, '')
|
||||
return app.text('$err')
|
||||
}
|
||||
return app.json(response)
|
||||
}
|
||||
```
|
||||
|
||||
#### - html
|
||||
|
||||
Response HTTP_OK with payload with content-type `text/html`
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
pub fn (mut app App) html_page() vweb.Result {
|
||||
return app.html('<h1>ok</h1>')
|
||||
}
|
||||
```
|
||||
|
||||
#### - text
|
||||
|
||||
Response HTTP_OK with payload with content-type `text/plain`
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
pub fn (mut app App) simple() vweb.Result {
|
||||
return app.text('A simple result')
|
||||
}
|
||||
```
|
||||
|
||||
#### - json
|
||||
|
||||
Response HTTP_OK with payload with content-type `application/json`
|
||||
**Examples:**
|
||||
|
||||
```v ignore
|
||||
['/articles'; get]
|
||||
pub fn (mut app App) articles() vweb.Result {
|
||||
articles := app.find_all_articles()
|
||||
json_result := json.encode(articles)
|
||||
return app.json(json_result)
|
||||
}
|
||||
```
|
||||
|
||||
```v ignore
|
||||
['/user/create'; post]
|
||||
pub fn (mut app App) controller_create_user() vweb.Result {
|
||||
body := json.decode(User, app.req.data) or {
|
||||
app.set_status(400, '')
|
||||
return app.text('Failed to decode json, error: $err')
|
||||
}
|
||||
|
||||
response := app.service_add_user(body.username, body.password) or {
|
||||
app.set_status(400, '')
|
||||
return app.text('error: $err')
|
||||
}
|
||||
|
||||
return app.json(response)
|
||||
}
|
||||
```
|
||||
|
||||
#### - json_pretty
|
||||
|
||||
Response HTTP_OK with a pretty-printed JSON result
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
fn (mut app App) time_json_pretty() {
|
||||
app.json_pretty({
|
||||
'time': time.now().format()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### - file
|
||||
|
||||
Response HTTP_OK with file as payload
|
||||
|
||||
#### - ok
|
||||
|
||||
Response HTTP_OK with payload
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
['/form_echo'; post]
|
||||
pub fn (mut app App) form_echo() vweb.Result {
|
||||
app.set_content_type(app.req.header.get(.content_type) or { '' })
|
||||
return app.ok(app.form['foo'])
|
||||
}
|
||||
```
|
||||
|
||||
#### - server_error
|
||||
|
||||
Response a server error
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
fn (mut app App) sse() vweb.Result {
|
||||
return app.server_error(501)
|
||||
}
|
||||
```
|
||||
|
||||
#### - not_found
|
||||
|
||||
Response HTTP_NOT_FOUND with payload
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
['/:user/:repo/settings']
|
||||
pub fn (mut app App) user_repo_settings(username string, repository string) vweb.Result {
|
||||
if username !in known_users {
|
||||
return app.not_found()
|
||||
}
|
||||
return app.html('username: $username | repository: $repository')
|
||||
}
|
||||
```
|
||||
|
||||
### Requests
|
||||
|
||||
#### - get_header
|
||||
|
||||
Returns the header data from the key
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
['/user/get_all'; get]
|
||||
pub fn (mut app App) controller_get_all_user() vweb.Result {
|
||||
token := app.get_header('token')
|
||||
return app.text(token)
|
||||
}
|
||||
```
|
||||
|
||||
#### - get_cookie
|
||||
|
||||
Sets a cookie
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
pub fn (mut app App) before_request() {
|
||||
app.user_id = app.get_cookie('id') or { '0' }
|
||||
}
|
||||
```
|
||||
|
||||
#### - add_header
|
||||
|
||||
Adds an header to the response with key and val
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
['/upload'; post]
|
||||
pub fn (mut app App) upload() vweb.Result {
|
||||
fdata := app.files['upfile']
|
||||
|
||||
data_rows := fdata[0].data.split('\n')
|
||||
|
||||
mut output_data := ''
|
||||
|
||||
for elem in data_rows {
|
||||
delim_row := elem.split('\t')
|
||||
output_data += '${delim_row[0]}\t${delim_row[1]}\t'
|
||||
output_data += '${delim_row[0].int() + delim_row[1].int()}\n'
|
||||
}
|
||||
|
||||
output_data = output_data.all_before_last('\n')
|
||||
|
||||
app.add_header('Content-Disposition', 'attachment; filename=results.txt')
|
||||
app.send_response_to_client('application/octet-stream', output_data)
|
||||
|
||||
return $vweb.html()
|
||||
}
|
||||
```
|
||||
|
||||
#### - set_cookie
|
||||
|
||||
Sets a cookie
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
pub fn (mut app App) cookie() vweb.Result {
|
||||
app.set_cookie(name: 'cookie', value: 'test')
|
||||
return app.text('Response Headers\n$app.header')
|
||||
}
|
||||
```
|
||||
|
||||
#### - set_cookie_with_expire_date
|
||||
|
||||
Sets a cookie with a `expire_data`
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
pub fn (mut app App) cookie() vweb.Result {
|
||||
key := 'cookie'
|
||||
value := 'test'
|
||||
duration := time.Duration(2 * time.minute ) // add 2 minutes
|
||||
expire_date := time.now().add(duration)
|
||||
|
||||
app.set_cookie_with_expire_date(key, value, expire_date)
|
||||
return app.text('Response Headers\n$app.header')
|
||||
}
|
||||
```
|
||||
|
||||
#### - set_content_type
|
||||
|
||||
Sets the response content type
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
['/form_echo'; post]
|
||||
pub fn (mut app App) form_echo() vweb.Result {
|
||||
app.set_content_type(app.req.header.get(.content_type) or { '' })
|
||||
return app.ok(app.form['foo'])
|
||||
}
|
||||
```
|
||||
|
||||
### Template
|
||||
|
||||
#### -handle_static
|
||||
|
||||
handle_static is used to mark a folder (relative to the current working folder) as one that
|
||||
contains only static resources (css files, images etc).
|
||||
|
||||
If `root` is set the mount path for the dir will be in '/'
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
app.serve_static('/favicon.ico', 'favicon.ico')
|
||||
// Automatically make available known static mime types found in given directory.
|
||||
os.chdir(os.dir(os.executable()))?
|
||||
app.handle_static('assets', true)
|
||||
vweb.run(app, port)
|
||||
}
|
||||
```
|
||||
|
||||
#### -mount_static_folder_at
|
||||
|
||||
makes all static files in `directory_path` and inside it, available at http://server/mount_path.
|
||||
|
||||
For example: suppose you have called .mount_static_folder_at('/var/share/myassets', '/assets'),
|
||||
and you have a file /var/share/myassets/main.css .
|
||||
=> That file will be available at URL: http://server/assets/main.css .
|
||||
|
||||
#### -serve_static
|
||||
|
||||
Serves a file static.
|
||||
`url` is the access path on the site, `file_path` is the real path to the file, `mime_type` is the
|
||||
file type
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
fn main() {
|
||||
mut app := &App{}
|
||||
app.serve_static('/favicon.ico', 'favicon.ico')
|
||||
app.mount_static_folder_at(os.resource_abs_path('.'), '/')
|
||||
vweb.run(app, 8081)
|
||||
}
|
||||
```
|
||||
|
||||
### Others
|
||||
|
||||
#### -ip
|
||||
|
||||
Returns the ip address from the current user
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
pub fn (mut app App) ip() vweb.Result {
|
||||
ip := app.ip()
|
||||
return app.text('ip: $ip')
|
||||
}
|
||||
```
|
||||
|
||||
#### -error
|
||||
|
||||
Set a string to the form error
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
pub fn (mut app App) error() vweb.Result {
|
||||
app.error('here as an error')
|
||||
println(app.form_error) //'vweb error: here as an error'
|
||||
}
|
||||
```
|
||||
|
@ -193,6 +193,7 @@ pub fn (ctx Context) init_server() {
|
||||
// Probably you can use it for check user session cookie or add header.
|
||||
pub fn (ctx Context) before_request() {}
|
||||
|
||||
// TODO - test
|
||||
// vweb intern function
|
||||
[manualfree]
|
||||
pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bool {
|
||||
@ -243,6 +244,7 @@ pub fn (mut ctx Context) json_pretty<T>(j T) Result {
|
||||
return Result{}
|
||||
}
|
||||
|
||||
// TODO - test
|
||||
// Response HTTP_OK with file as payload
|
||||
pub fn (mut ctx Context) file(f_path string) Result {
|
||||
ext := os.file_ext(f_path)
|
||||
@ -267,6 +269,7 @@ pub fn (mut ctx Context) ok(s string) Result {
|
||||
return Result{}
|
||||
}
|
||||
|
||||
// TODO - test
|
||||
// Response a server error
|
||||
pub fn (mut ctx Context) server_error(ecode int) Result {
|
||||
$if debug {
|
||||
@ -302,6 +305,7 @@ pub fn (mut ctx Context) not_found() Result {
|
||||
return Result{}
|
||||
}
|
||||
|
||||
// TODO - test
|
||||
// Sets a cookie
|
||||
pub fn (mut ctx Context) set_cookie(cookie http.Cookie) {
|
||||
mut cookie_data := []string{}
|
||||
@ -320,6 +324,7 @@ pub fn (mut ctx Context) set_content_type(typ string) {
|
||||
ctx.content_type = typ
|
||||
}
|
||||
|
||||
// TODO - test
|
||||
// Sets a cookie with a `expire_data`
|
||||
pub fn (mut ctx Context) set_cookie_with_expire_date(key string, val string, expire_date time.Time) {
|
||||
ctx.add_header('Set-Cookie', '$key=$val; Secure; HttpOnly; expires=$expire_date.utc_string()')
|
||||
@ -345,6 +350,7 @@ pub fn (ctx &Context) get_cookie(key string) ?string { // TODO refactor
|
||||
return error('Cookie not found')
|
||||
}
|
||||
|
||||
// TODO - test
|
||||
// Sets the response status
|
||||
pub fn (mut ctx Context) set_status(code int, desc string) {
|
||||
if code < 100 || code > 599 {
|
||||
@ -354,11 +360,13 @@ pub fn (mut ctx Context) set_status(code int, desc string) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - test
|
||||
// Adds an header to the response with key and val
|
||||
pub fn (mut ctx Context) add_header(key string, val string) {
|
||||
ctx.header.add_custom(key, val) or {}
|
||||
}
|
||||
|
||||
// TODO - test
|
||||
// Returns the header data from the key
|
||||
pub fn (ctx &Context) get_header(key string) string {
|
||||
return ctx.req.header.get_custom(key) or { '' }
|
||||
@ -381,7 +389,7 @@ pub struct RunParams {
|
||||
}
|
||||
|
||||
// run_at - start a new VWeb server, listening only on a specific address `host`, at the specified `port`
|
||||
// Example: vweb.run_at(app, 'localhost', 8099)
|
||||
// Example: vweb.run_at(new_app(), vweb.RunParams{ host: 'localhost' port: 8099 family: .ip }) or { panic(err) }
|
||||
[manualfree]
|
||||
pub fn run_at<T>(global_app &T, params RunParams) ? {
|
||||
if params.port <= 0 || params.port > 65535 {
|
||||
@ -653,6 +661,7 @@ pub fn (mut ctx Context) handle_static(directory_path string, root bool) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO - test
|
||||
// mount_static_folder_at - makes all static files in `directory_path` and inside it, available at http://server/mount_path
|
||||
// For example: suppose you have called .mount_static_folder_at('/var/share/myassets', '/assets'),
|
||||
// and you have a file /var/share/myassets/main.css .
|
||||
@ -668,6 +677,7 @@ pub fn (mut ctx Context) mount_static_folder_at(directory_path string, mount_pat
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO - test
|
||||
// Serves a file static
|
||||
// `url` is the access path on the site, `file_path` is the real path to the file, `mime_type` is the file type
|
||||
pub fn (mut ctx Context) serve_static(url string, file_path string) {
|
||||
|
Loading…
Reference in New Issue
Block a user