Skip to content

Commit

Permalink
os, net.http.file: add a folder listing to the http static file serve…
Browse files Browse the repository at this point in the history
…r, started by file.serve/1 (vlang#20192)
  • Loading branch information
spytheman authored Dec 16, 2023
1 parent 687b33a commit de3b2b0
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 46 deletions.
28 changes: 28 additions & 0 deletions vlib/net/http/file/entity.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module file

import os
import time

pub struct Entity {
os.FileInfo
path string
mod_time time.Time
url string
fname string
}

fn path_to_entity(path string, uri_path string) Entity {
pinfo := os.inode(path)
mut uri_base := ''
if uri_path.len > 0 {
uri_base = '/${uri_path}'
}
fname := os.file_name(path)
return Entity{
FileInfo: pinfo
path: path
mod_time: time.unix(pinfo.mtime)
url: '${uri_base}/${fname}'
fname: fname
}
}
118 changes: 118 additions & 0 deletions vlib/net/http/file/folder_index.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
module file

import os
import time
import strings

const myexe = os.executable()
const myexe_prefix = os.file_name(myexe.all_before_last('.'))

fn get_folder_index_html(requested_file_path string, uri_path string, filter_myexe bool) string {
sw := time.new_stopwatch()
mut files := os.ls(requested_file_path) or { [] }
if filter_myexe {
files = files.filter(!it.contains(file.myexe_prefix))
}
mut sb := strings.new_builder(files.len * 200)
write_page_header(mut sb, uri_path)
write_page_crumbs(mut sb, uri_path)
write_page_table(mut sb, uri_path, requested_file_path, mut files)
sb.writeln('<p>Server time: <b>${time.now().format_ss()}</b>, generated in <b>${sw.elapsed().microseconds():6} us</b></p>')
write_page_footer(mut sb, uri_path)
return sb.str()
}

fn write_page_header(mut sb strings.Builder, uri_path string) {
// html boilerplate for the header
sb.writeln('<!DOCTYPE html>')
sb.writeln('<html lang="en">')
sb.writeln('<head>')
sb.writeln('<meta charset="utf-8">')
sb.writeln('<title>Index of local folder ${uri_path}</title>')
sb.writeln('<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />')
sb.writeln('</head>')
sb.writeln('<body>')
}

fn write_page_footer(mut sb strings.Builder, uri_path string) {
// html boilerplate for the footer
sb.writeln('</body>')
sb.writeln('</html>')
}

fn write_page_crumbs(mut sb strings.Builder, uri_path string) {
crumbs := uri_path.split('/')
mut crlinks := []string{cap: crumbs.len}
for cridx, crumb in crumbs {
cr_so_far := crumbs#[0..cridx + 1].join('/')
// eprintln('> cr_so_far: ${cr_so_far:20} | crumb: ${crumb:20}')
crlinks << '<a href="/${cr_so_far}">${crumb}</a>'
}
crlinks_text := crlinks.join('&nbsp;/&nbsp;')
sb.writeln('<h2>Index of <a href="/">/</a>&nbsp;${crlinks_text}&nbsp;:</h2>')
}

fn write_page_table(mut sb strings.Builder, uri_path string, requested_file_path string, mut files []string) {
files.sort()
sb.writeln('<table>')
sb.writeln('<tr>')
sb.writeln('<th align="left" style="width: 100px">Size</th>')
sb.writeln('<th align="left" style="width: 200px">Last modified</th>')
sb.writeln('<th align="left">Name</th>')
sb.writeln('</tr>')
if uri_path.len == 0 {
sb.writeln('<tr>')
sb.writeln('<td>---</td>')
sb.writeln('<td>---</td>')
sb.writeln('<td>---</td>')
sb.writeln('</tr>')
} else {
sb.writeln('<tr>')
sb.writeln('<td></td>')
sb.writeln('<td></td>')
sb.writeln('<td><a href="/${uri_path}/..">..</td>')
sb.writeln('</tr>')
}
mut entities := []Entity{cap: files.len}
for fname in files {
path := os.join_path(requested_file_path, fname)
entities << path_to_entity(path, uri_path)
}
entities.sort_with_compare(fn (a &Entity, b &Entity) int {
if a.typ == b.typ {
if a.fname < b.fname {
return -1
}
if a.fname > b.fname {
return 1
}
return 0
}
if a.typ == .directory {
return -1
}
return 1
})
for entity in entities {
if entity.typ == .directory {
sb.writeln('<tr>')
sb.writeln('<td></td>')
sb.writeln('<td>${entity.mod_time.format_ss()}</td>')
sb.writeln('<td><a href="${entity.url}">${entity.fname}/</a></td>')
sb.writeln('</tr>')
} else if entity.typ == .symbolic_link {
sb.writeln('<tr>')
sb.writeln('<td>${entity.size}</td>')
sb.writeln('<td>${entity.mod_time.format_ss()}</td>')
sb.writeln('<td><a href="${entity.url}">${entity.fname}@</a></td>')
sb.writeln('</tr>')
} else {
sb.writeln('<tr>')
sb.writeln('<td>${entity.size}</td>')
sb.writeln('<td>${entity.mod_time.format_ss()}</td>')
sb.writeln('<td><a href="${entity.url}">${entity.fname}</a></td>')
sb.writeln('</tr>')
}
}
sb.writeln('</table>')
}
27 changes: 18 additions & 9 deletions vlib/net/http/file/static_server.v
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mut:
on string = 'localhost:4001' // on which address:port to listen for http requests
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
shutdown_after time.Duration = time.infinite // after this time has passed, the webserver will gracefully shutdown on its own
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()'`
}

// serve will start a static files web server.
Expand Down Expand Up @@ -57,31 +58,39 @@ fn (mut h StaticHttpHandler) handle(req http.Request) http.Response {
mut res := http.new_response(body: '')
sw := time.new_stopwatch()
defer {
log.info('took: ${sw.elapsed().microseconds():6} us, status: ${res.status_code}, size: ${res.body.len:6}, url: ${req.url}')
log.info('took: ${sw.elapsed().microseconds():6} us, status: ${res.status_code}, size: ${res.body.len:9}, url: ${req.url}')
}
mut uri_path := req.url.all_after_first('/').trim_right('/')
requested_file_path := os.norm_path(os.real_path(os.join_path_single(h.params.folder,
req.url.all_after_first('/'))))
uri_path)))
if !requested_file_path.starts_with(h.params.folder) {
log.warn('forbidden request; base folder: ${h.params.folder}, requested_file_path: ${requested_file_path}, ')
res = http.new_response(body: '<h1>forbidden</h1>')
res.set_status(.forbidden)
res.header.add(.content_type, 'text/html; charset=utf-8')
return res
}
mut body := ''
if !os.exists(requested_file_path) {
res.set_status(.not_found)
res.body = '<!DOCTYPE html><h1>no such file</h1>'
res.header.add(.content_type, 'text/html; charset=utf-8')
return res
}
body = os.read_file(requested_file_path) or {
res.set_status(.not_found)
''
//
mut body := ''
mut content_type := 'text/html; charset=utf-8'
if os.is_dir(requested_file_path) {
body = get_folder_index_html(requested_file_path, uri_path, h.params.filter_myexe)
} else {
body = os.read_file(requested_file_path) or {
res.set_status(.not_found)
''
}
mt := mime.get_mime_type(os.file_ext(requested_file_path).all_after_first('.'))
content_type = mime.get_content_type(mt)
}
mt := mime.get_mime_type(os.file_ext(requested_file_path).all_after_first('.'))
ct := mime.get_content_type(mt)
res = http.new_response(body: body)
res.body = body
res.header.add(.content_type, ct)
res.header.add(.content_type, content_type)
return res
}
5 changes: 0 additions & 5 deletions vlib/os/file.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ pub mut:
is_opened bool
}

struct FileInfo {
name string
size int
}

fn C.fseeko(&C.FILE, u64, int) int

fn C._fseeki64(&C.FILE, u64, int) int
Expand Down
84 changes: 52 additions & 32 deletions vlib/os/inode.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,23 @@
// that can be found in the LICENSE file.
module os

pub struct FileMode {
pub:
typ FileType
owner FilePermission
group FilePermission
others FilePermission
}

pub struct FileInfo {
FileMode
pub:
size u64 // size of the file in bytes
mtime i64 // last modification time in seconds after the Unix epoch
}

pub enum FileType {
unknown
regular
directory
character_device
Expand All @@ -13,7 +29,7 @@ pub enum FileType {
socket
}

struct FilePermission {
pub struct FilePermission {
pub:
read bool
write bool
Expand All @@ -36,45 +52,28 @@ pub fn (p FilePermission) bitmask() u32 {
return mask
}

pub struct FileMode {
pub:
typ FileType
owner FilePermission
group FilePermission
others FilePermission
}

// bitmask returns a 9 bit sequence in the order owner + group + others.
// This is a valid bitmask to use with `os.chmod`.
pub fn (m FileMode) bitmask() u32 {
return m.owner.bitmask() << 6 | m.group.bitmask() << 3 | m.others.bitmask()
}

// inode returns the mode of the file/inode containing inode type and permission information
// it supports windows for regular files but it doesn't matter if you use owner, group or others when checking permissions on windows
pub fn inode(path string) FileMode {
// inode returns the metadata of the file/inode, containing inode type, permission information, size and modification time.
// it supports windows for regular files, but it doesn't matter if you use owner, group or others when checking permissions on windows.
pub fn inode(path string) FileInfo {
mut attr := C.stat{}
unsafe { C.stat(&char(path.str), &attr) }
mut typ := FileType.regular
if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFDIR) {
typ = .directory
}
$if !windows {
if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFCHR) {
typ = .character_device
} else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFBLK) {
typ = .block_device
} else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFIFO) {
typ = .fifo
} else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFLNK) {
typ = .symbolic_link
} else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFSOCK) {
typ = .socket
}
}
$if windows {
return FileMode{
// TODO: replace this with a C.GetFileAttributesW call instead.
// Use stat, lstat is not available on windows
unsafe { C.stat(&char(path.str), &attr) }
mut typ := FileType.regular
if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFDIR) {
typ = .directory
}
return FileInfo{
typ: typ
size: attr.st_size
mtime: attr.st_mtime
owner: FilePermission{
read: (attr.st_mode & u32(C.S_IREAD)) != 0
write: (attr.st_mode & u32(C.S_IWRITE)) != 0
Expand All @@ -92,8 +91,29 @@ pub fn inode(path string) FileMode {
}
}
} $else {
return FileMode{
// note, that we use lstat here on purpose, to know the information about
// the potential symlinks themselves, not about the entities they point at
unsafe { C.lstat(&char(path.str), &attr) }
mut typ := FileType.unknown
if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFREG) {
typ = .regular
} else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFDIR) {
typ = .directory
} else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFCHR) {
typ = .character_device
} else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFBLK) {
typ = .block_device
} else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFIFO) {
typ = .fifo
} else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFLNK) {
typ = .symbolic_link
} else if attr.st_mode & u32(C.S_IFMT) == u32(C.S_IFSOCK) {
typ = .socket
}
return FileInfo{
typ: typ
size: attr.st_size
mtime: attr.st_mtime
owner: FilePermission{
read: (attr.st_mode & u32(C.S_IRUSR)) != 0
write: (attr.st_mode & u32(C.S_IWUSR)) != 0
Expand Down
7 changes: 7 additions & 0 deletions vlib/os/os.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,7 @@ pub fn is_link(path string) bool {

struct PathKind {
mut:
is_file bool
is_dir bool
is_link bool
}
Expand All @@ -812,6 +813,9 @@ fn kind_of_existing_path(path string) PathKind {
$if windows {
attr := C.GetFileAttributesW(path.to_wide())
if attr != u32(C.INVALID_FILE_ATTRIBUTES) {
if (int(attr) & C.FILE_ATTRIBUTE_NORMAL) != 0 {
res.is_file = true
}
if (int(attr) & C.FILE_ATTRIBUTE_DIRECTORY) != 0 {
res.is_dir = true
}
Expand All @@ -825,6 +829,9 @@ fn kind_of_existing_path(path string) PathKind {
res_stat := unsafe { C.lstat(&char(path.str), &statbuf) }
if res_stat == 0 {
kind := (int(statbuf.st_mode) & s_ifmt)
if kind == s_ifreg {
res.is_file = true
}
if kind == s_ifdir {
res.is_dir = true
}
Expand Down
2 changes: 2 additions & 0 deletions vlib/os/os_nix.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub const s_ifmt = 0xF000 // type of file

pub const s_ifdir = 0x4000 // directory

pub const s_ifreg = 0x8000 // regular file

pub const s_iflnk = 0xa000 // link

pub const s_isuid = 0o4000 // SUID
Expand Down

0 comments on commit de3b2b0

Please sign in to comment.