diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..517d63e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.v]
+indent_style = tab
+indent_size = 4
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..885103d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+* text=auto eol=lf
+*.bat eol=crlf
+
+**/*.v linguist-language=V
+**/*.vv linguist-language=V
+**/*.vsh linguist-language=V
+**/v.mod linguist-language=V
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 552a506..7fcb2f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
# ---> V
+main
+*.dylib
+*.exe~
*.exe
*.o
*.so
@@ -11,3 +14,9 @@
*.bak
*.out
+bin/
+
+.DS_Store
+.idea/
+.vscode/
+*.iml
diff --git a/app/Dockerfile b/app/Dockerfile
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/assets/dev/index.html b/app/src/assets/dev/index.html
new file mode 100644
index 0000000..454e480
--- /dev/null
+++ b/app/src/assets/dev/index.html
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/assets/dev/ref.html b/app/src/assets/dev/ref.html
new file mode 100644
index 0000000..fe4f7c4
--- /dev/null
+++ b/app/src/assets/dev/ref.html
@@ -0,0 +1,260 @@
+
+
+
+
+
+
File Browser
+
+
+
+
+
+
+
+
+
diff --git a/app/src/assets/style/error.css b/app/src/assets/style/error.css
new file mode 100644
index 0000000..4e03a8c
--- /dev/null
+++ b/app/src/assets/style/error.css
@@ -0,0 +1,101 @@
+
+* { margin:0; padding:0; box-sizing:border-box; }
+
+:root[data-theme="light"] {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f5f7fb;
+ --bg-tertiary: #eff2f5;
+ --text-primary: #1a202c;
+ --text-secondary: #6c757d;
+ --border-color: #d4d7de;
+ --accent: #0051ba;
+ --accent-hover: #003e8f;
+}
+
+:root[data-theme="dark"] {
+ --bg-primary: #0d1117;
+ --bg-secondary: #161b22;
+ --bg-tertiary: #21262d;
+ --text-primary: #e6edf3;
+ --text-secondary: #8b949e;
+ --border-color: #30363d;
+ --accent: #58a6ff;
+ --accent-hover: #79c0ff;
+}
+
+:root {
+ --bg-primary:#0d1117;
+ --bg-secondary:#161b22;
+ --bg-tertiary:#21262d;
+ --text-primary:#e6edf3;
+ --text-secondary:#8b949e;
+ --border-color:#30363d;
+ --accent:#58a6ff;
+ --accent-hover:#79c0ff;
+
+ @media (prefers-color-scheme: light) {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f5f7fb;
+ --bg-tertiary: #eff2f5;
+ --text-primary: #1a202c;
+ --text-secondary: #6c757d;
+ --border-color: #d4d7de;
+ --accent: #0051ba;
+ --accent-hover: #003e8f;
+ }
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ flex-direction: column;
+ text-align: center;
+ transition: background-color 0.3s, color 0.3s;
+}
+
+h1 {
+ font-size: 6rem;
+ margin-bottom: 1rem;
+ color: var(--accent);
+}
+
+p {
+ font-size: 1.25rem;
+ margin-bottom: 2rem;
+ color: var(--text-secondary);
+}
+
+a.btn {
+ display: inline-block;
+ padding: 0.75rem 1.5rem;
+ background-color: var(--bg-tertiary);
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 0.375rem;
+ text-decoration: none;
+ transition: all 0.2s ease;
+}
+
+a.btn:hover {
+ background-color: var(--accent);
+ border-color: var(--accent);
+ color: #fff;
+}
+
+@media (prefers-color-scheme: light) {
+ :root:not([data-theme]) {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f5f7fb;
+ --bg-tertiary: #eff2f5;
+ --text-primary: #1a202c;
+ --text-secondary: #6c757d;
+ --border-color: #d4d7de;
+ --accent: #0051ba;
+ --accent-hover: #003e8f;
+ }
+}
diff --git a/app/src/assets/style/style.css b/app/src/assets/style/style.css
new file mode 100644
index 0000000..7449460
--- /dev/null
+++ b/app/src/assets/style/style.css
@@ -0,0 +1,476 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+/* Light Mode (Cloudflare Dashboard Style) */
+:root[data-theme="light"] {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f5f7fb;
+ --bg-tertiary: #eff2f5;
+ --text-primary: #1a202c;
+ --text-secondary: #6c757d;
+ --border-color: #d4d7de;
+ --accent: #0051ba;
+ --accent-hover: #003e8f;
+ --file-hover: #f0f2f5;
+}
+
+/* Dark Mode (GitHub Style) */
+:root[data-theme="dark"] {
+ --bg-primary: #0d1117;
+ --bg-secondary: #161b22;
+ --bg-tertiary: #21262d;
+ --text-primary: #e6edf3;
+ --text-secondary: #8b949e;
+ --border-color: #30363d;
+ --accent: #58a6ff;
+ --accent-hover: #79c0ff;
+ --file-hover: #1c2128;
+}
+
+/* Default to dark mode */
+:root {
+ --bg-primary: #0d1117;
+ --bg-secondary: #161b22;
+ --bg-tertiary: #21262d;
+ --text-primary: #e6edf3;
+ --text-secondary: #8b949e;
+ --border-color: #30363d;
+ --accent: #58a6ff;
+ --accent-hover: #79c0ff;
+ --file-hover: #1c2128;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ line-height: 1.6;
+ transition: background-color 0.3s ease, color 0.3s ease;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.header {
+ background-color: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-color);
+ padding: 1.5rem;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ transition: background-color 0.3s ease, border-color 0.3s ease;
+}
+
+.breadcrumbs {
+ text-transform: uppercase;
+ font-size: 10px;
+ letter-spacing: 1px;
+ color: var(--text-secondary);
+ margin-bottom: 0.5rem;
+ padding-left: 3px;
+}
+
+.path-container {
+ display: flex;
+ align-items: center;
+ gap: 0;
+ flex-wrap: wrap;
+ word-break: break-all;
+ margin-bottom: 1rem;
+ font-size: 1rem;
+}
+
+.breadcrumb-link {
+ color: var(--accent);
+ text-decoration: none;
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.25rem;
+ transition: all 0.2s ease;
+ cursor: pointer;
+}
+
+.breadcrumb-link:hover {
+ background-color: var(--bg-tertiary);
+ color: var(--accent-hover);
+}
+
+.breadcrumb-text {
+ color: var(--text-secondary);
+ padding: 0.25rem 0.5rem;
+}
+
+.breadcrumb-sep {
+ color: var(--text-secondary);
+ margin: 0 0.25rem;
+}
+
+.controls {
+ display: flex;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+}
+
+.btn {
+ padding: 0.5rem 1rem;
+ background-color: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ border-radius: 0.375rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+}
+
+.btn:hover {
+ background-color: var(--accent);
+ border-color: var(--accent);
+ color: #ffffff;
+}
+
+.btn:active {
+ transform: scale(0.98);
+}
+
+.content {
+ padding: 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 1rem;
+ border-bottom: 1px solid var(--border-color);
+ background-color: var(--bg-secondary);
+}
+
+.meta {
+ display: flex;
+ gap: 2rem;
+ font-size: 0.875rem;
+ flex-wrap: wrap;
+}
+
+#summary {
+ display: flex;
+ gap: 2rem;
+ align-items: center;
+}
+
+.meta-item {
+ white-space: nowrap;
+}
+
+.meta-item b {
+ color: var(--text-primary);
+ font-weight: 600;
+}
+
+.layout-toggle {
+ display: flex;
+ gap: 0.5rem;
+ border: 1px solid var(--border-color);
+ border-radius: 0.375rem;
+ padding: 0.25rem;
+ background-color: var(--bg-tertiary);
+}
+
+.layout-btn {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.375rem 0.75rem;
+ background: transparent;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ border-radius: 0.25rem;
+ transition: all 0.2s ease;
+ font-size: 0.875rem;
+}
+
+.layout-btn:hover {
+ color: var(--text-primary);
+ background-color: var(--bg-secondary);
+}
+
+.layout-btn.active {
+ background-color: var(--accent);
+ color: white;
+}
+
+.listing {
+ flex: 1;
+ padding: 1.5rem;
+ overflow-x: auto;
+}
+
+/* Table Layout */
+.file-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.file-table thead {
+ position: sticky;
+ top: 0;
+ background-color: var(--bg-secondary);
+ z-index: 5;
+}
+
+.file-table th {
+ text-align: left;
+ padding: 0.75rem;
+ border-bottom: 1px solid var(--border-color);
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.file-table tbody tr {
+ border-bottom: 1px solid var(--border-color);
+ transition: all 0.2s ease;
+ cursor: pointer;
+}
+
+.file-table tbody tr:hover {
+ background-color: var(--file-hover);
+}
+
+.file-table td {
+ padding: 0.75rem;
+ color: var(--text-primary);
+}
+
+.file-table td.icon {
+ width: 2.5rem;
+ font-size: 1.25rem;
+ text-align: center;
+}
+
+.file-table td.name {
+ font-weight: 500;
+}
+
+.file-table tr.directory td.name {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.file-table td.size,
+.file-table td.date {
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+ white-space: nowrap;
+}
+
+/* Grid Layout */
+.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 1rem;
+}
+
+.grid-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 1rem;
+ background-color: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: 0.5rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-align: center;
+}
+
+.grid-item:hover {
+ background-color: var(--file-hover);
+ border-color: var(--accent);
+ transform: translateY(-2px);
+}
+
+.grid-icon {
+ font-size: 2.5rem;
+ margin-bottom: 0.75rem;
+}
+
+.grid-name {
+ font-weight: 500;
+ color: var(--text-primary);
+ word-break: break-word;
+ overflow-wrap: break-word;
+ font-size: 0.875rem;
+ margin-bottom: 0.5rem;
+}
+
+.grid-item:has(.grid-name) .grid-name {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.grid-size {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ margin-top: auto;
+}
+
+.empty-state {
+ text-align: center;
+ padding: 3rem 1rem;
+ color: var(--text-secondary);
+}
+
+.empty-state-icon {
+ font-size: 3rem;
+ margin-bottom: 1rem;
+ opacity: 0.5;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .header {
+ padding: 1rem;
+ }
+
+ .listing {
+ padding: 1rem;
+ }
+
+ .content {
+ padding: 1rem;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .file-table td.date {
+ display: none;
+ }
+
+ .file-table th:last-child {
+ display: none;
+ }
+
+ .grid {
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+ gap: 0.75rem;
+ }
+
+ .grid-item {
+ padding: 0.75rem;
+ }
+
+ .path-container {
+ font-size: 0.9rem;
+ margin-bottom: 0.75rem;
+ }
+
+ .meta {
+ gap: 1rem;
+ font-size: 0.8rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .container {
+ min-height: auto;
+ }
+
+ .header {
+ padding: 0.75rem;
+ }
+
+ .listing {
+ padding: 0.75rem;
+ }
+
+ .content {
+ padding: 0.75rem;
+ gap: 0.5rem;
+ }
+
+ .path-container {
+ font-size: 0.8rem;
+ margin-bottom: 0.5rem;
+ }
+
+ .breadcrumb-link,
+ .breadcrumb-text,
+ .breadcrumb-sep {
+ padding: 0.125rem 0.25rem;
+ }
+
+ .controls {
+ width: 100%;
+ gap: 0.5rem;
+ }
+
+ .btn {
+ flex: 1;
+ padding: 0.5rem;
+ font-size: 0.75rem;
+ }
+
+ .meta {
+ gap: 1rem;
+ font-size: 0.75rem;
+ flex-direction: column;
+ }
+
+ .file-table th,
+ .file-table td {
+ padding: 0.5rem 0.25rem;
+ }
+
+ .file-table td.size {
+ display: none;
+ }
+
+ .grid {
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
+ gap: 0.5rem;
+ }
+
+ .grid-item {
+ padding: 0.5rem;
+ }
+
+ .grid-icon {
+ font-size: 2rem;
+ margin-bottom: 0.5rem;
+ }
+
+ .grid-name {
+ font-size: 0.7rem;
+ }
+}
+
+/* Scrollbar Styling */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-secondary);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border-color);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--text-secondary);
+}
diff --git a/app/src/assets/style/style.css.zst b/app/src/assets/style/style.css.zst
new file mode 100644
index 0000000..3fe1c4e
Binary files /dev/null and b/app/src/assets/style/style.css.zst differ
diff --git a/app/src/main.v b/app/src/main.v
new file mode 100644
index 0000000..878cd33
--- /dev/null
+++ b/app/src/main.v
@@ -0,0 +1,126 @@
+module main
+
+import veb
+import os
+import log
+
+struct Config {
+pub mut:
+ root string
+ port int
+}
+
+struct Embedded {
+pub mut:
+ style_css string
+ error_css string
+}
+
+pub struct User {
+pub mut:
+ name string
+ id int
+}
+
+pub struct Context {
+ veb.Context
+pub mut:
+ embed Embedded
+ user User
+ session_id string
+}
+
+pub struct App {
+ veb.Controller
+ veb.StaticHandler
+ veb.Middleware[Context]
+pub mut:
+ embed Embedded
+}
+
+pub struct Auth {}
+
+// endpoints
+
+@['/:path...']
+pub fn (app &App) root(mut ctx Context, path string) veb.Result {
+ style := app.embed.style_css
+ return ctx.html($tmpl('template/dashboard.html'))
+}
+
+@['/error']
+pub fn (app &App) error(mut ctx Context) veb.Result {
+ return ctx.html(ctx.error_page(500, 'Internal Server Error', 'Oops! Seems like something went wrong here.'))
+}
+
+// auth endpoints
+
+@[get; post]
+pub fn (auth &Auth) login(mut ctx Context) veb.Result {
+ return ctx.text('')
+}
+
+@[get; post]
+pub fn (auth &Auth) logout(mut ctx Context) veb.Result {
+ return ctx.text('')
+}
+
+@[get; post; put]
+pub fn (auth &Auth) register(mut ctx Context) veb.Result {
+ return ctx.text('')
+}
+
+// utility
+
+fn (mut ctx Context) error_page(code int, short string, long string) string {
+ style := ctx.embed.error_css
+ return $tmpl('template/error.html')
+}
+
+pub fn (mut ctx Context) not_found() veb.Result {
+ ctx.res.set_status(.not_found)
+ return ctx.html(ctx.error_page(404, 'Not found', 'Oops! The page you are looking for does not exist.'))
+}
+
+// --
+
+fn populate() &Config {
+ mut cfg := &Config{
+ root: $d('root', '.')
+ port: $d('port', 6767)
+ }
+
+ if os.getenv('CDN_ROOT') != '' {
+ cfg.root = os.getenv('CDN_ROOT').str()
+ }
+ if os.getenv('CDN_PORT') != '' {
+ cfg.port = os.getenv('CDN_PORT').int()
+ }
+
+ return cfg
+}
+
+fn main() {
+ cfg := populate()
+
+ mut app := &App{}
+ mut auth := &Auth{}
+ app.embed = &Embedded{
+ style_css: $embed_file('assets/style/style.css', .zlib).to_string()
+ error_css: $embed_file('assets/style/error.css', .zlib).to_string()
+ }
+
+ app.register_controller[Auth, Context]('/auth', mut auth)!
+
+ app.enable_static_compression = true
+ app.use(veb.encode_auto[Context]())
+
+ app.use(veb.MiddlewareOptions[Context]{
+ handler: fn [app] (mut ctx Context) bool {
+ ctx.embed = &app.embed
+ return true
+ }
+ })
+
+ veb.run[App, Context](mut app, cfg.port)
+}
diff --git a/app/src/template/dashboard.html b/app/src/template/dashboard.html
new file mode 100644
index 0000000..80704af
--- /dev/null
+++ b/app/src/template/dashboard.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
File Browser
+
+
+
+
+
+
+
diff --git a/app/src/template/error.html b/app/src/template/error.html
new file mode 100644
index 0000000..df0b2fa
--- /dev/null
+++ b/app/src/template/error.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
@code - @short
+
+
+
+
@code
+
@long
+
Go Home
+
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..f9059ef
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,32 @@
+services:
+ app:
+ build:
+ context: ./app
+ dockerfile: Dockerfile
+ container_name: cdn_web
+ depends_on:
+ - db
+ ports:
+ - "8080:8080"
+ environment:
+ # env shee
+ DB_HOST: db
+ DB_USER: appuser
+ DB_PASS: s3cret
+ DB_NAME: appdb
+ restart: unless-stopped
+
+ db:
+ image: postgres:16
+ container_name: cdn_db
+ environment:
+ POSTGRES_USER: appuser
+ POSTGRES_PASSWORD: s3cret
+ POSTGRES_DB: appdb
+ volumes:
+ - dbdata:/var/lib/postgresql/data
+ - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
+ restart: unless-stopped
+
+volumes:
+ dbdata:
diff --git a/v.mod b/v.mod
new file mode 100644
index 0000000..38fbabf
--- /dev/null
+++ b/v.mod
@@ -0,0 +1,7 @@
+Module {
+ name: 'CDN'
+ description: ''
+ version: '0.0.1'
+ license: 'MIT'
+ dependencies: []
+}