start resty port to lapis

This commit is contained in:
dreamer 2025-11-06 16:37:55 +01:00
commit 8ab812dce0
14 changed files with 878 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_data/*

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM openresty/openresty:1.27.1.2-4-alpine-fat
ENV LAPIS_VERSION=1.16.0
RUN apk add openssl-dev git
RUN apk --no-cache add findutils
RUN apk --no-cache add coreutils
RUN opm get spacewander/luafilesystem
RUN luarocks install luasec
RUN luarocks install busted
RUN luarocks install lapis ${LAPIS_VERSION}
RUN luarocks install moonscript
WORKDIR /srv/lapis
CMD lapis server $ENVIRONMENT

42
code/app.lua Normal file
View File

@ -0,0 +1,42 @@
local lapis = require("lapis")
local config = require("lapis.config").get()
local json_params = require("lapis.application").json_params
local app = lapis.Application()
app:enable("etlua")
app.layout = require "views.layout"
local autoload = require("lapis.util").autoload
local handlers = autoload("handlers")
local page_titles = {}
if string.find(ngx.var.host, "panamaracing.club") then
page_titles = {
name = "Panama Racing Club Archive",
url = "panamaracing.club"
}
elseif string.find(ngx.var.host, "videohotmix.net") then
page_titles = {
name = "Hotmix Video Archive",
url = "videohotmix.net"
}
else
page_titles = {
name = "Local Test Archive",
url = "localhost"
}
end
app:get("/", function(self)
self.titles = page_titles
return handlers.Roothandler(self)
end)
app:get("/*", function(self)
self.titles = page_titles
return handlers.Roothandler(self)
end)
return app

13
code/config.lua Normal file
View File

@ -0,0 +1,13 @@
local config = require("lapis.config")
config({"development", "production"}, {
host = "Video Hotmix Archive",
greeting = "Welcome, We Are Your Friends",
mount = "/mnt/data/",
})
config("production", {
greeting = "nothing here yet",
logging = {
queries = false
}
})

View File

@ -0,0 +1,27 @@
local to_json = require("lapis.util").to_json
local autoload = require("lapis.util").autoload
local config = require("lapis.config")
-- print(to_json(config.get()))
local hotmixes = autoload("hotmixes")
local function Roothandler(self)
local stuff = hotmixes.utils.these_files( hotmixes.utils.data_path )
local latest_path, latest_name = hotmixes.utils.these_latest()
self.total = hotmixes.utils.total_files_dir( config.get().mount )
self.uri = hotmixes.utils.request_path
self.path = '/data' .. hotmixes.utils.request_path
self.dirs = stuff.dirs
self.files = stuff.files
self.images = stuff.images
self.latestpath = latest_path
self.latestname = latest_name
return { render = "root" }
end
return Roothandler

125
code/hotmixes/utils.lua Normal file
View File

@ -0,0 +1,125 @@
local lfs = require 'lfs_ffi'
local config = require("lapis.config").get()
-- setup
local request_uri = ngx.var.request_uri
request_uri = ngx.unescape_uri(request_uri)
ngx.log(ngx.ERR, request_uri)
if string.find(request_uri, "\"") or string.find(request_uri, "%`") or string.find(request_uri, "%$") then
ngx.log(ngx.ERR, "Illegal request_uri")
ngx.exit(500)
end
local request_path
if request_uri ~= '/' then
request_path = request_uri .. '/'
else
request_path = request_uri
end
local data_dir = config.mount
local data_path = data_dir .. request_path
-- setup
local utils = {}
utils['request_path'] = request_path
utils['data_path'] = data_path
-- we want to know if something is an image
utils['match_image'] = function( file )
local filext = file:match("[^.]+$")
local extensions = { jpg=true, jpeg=true, png=true, gif=true }
if extensions[filext:lower()] then
return true
else
return false
end
end
utils['latest_files'] = function(directory)
local i, t, popen = 0, {}, io.popen
local pfile = popen('find "'..directory..'" -type f ! -name \'*.filepart\' -printf \'%C@ %p\n\'| sort -nr | head -7 | cut -f2- -d" "| sed s:"'..directory..'/"::')
for filename in pfile:lines() do
i = i + 1
t[i] = filename
end
pfile:close()
return t
end
utils['these_files'] = function( path )
local files, dirs, images = {}, {}, {}
for file in lfs.dir( path ) do
if file ~= "." and file ~= ".." and not string.match(file, ".filepart") then
if lfs.attributes( path .. file, "mode" ) == "file" then
if utils.match_image( file ) then
table.insert( images, file )
else
table.insert( files, file )
end
elseif lfs.attributes( path .. file, "mode" ) == "directory" then
table.insert( dirs, file )
end
end
end
table.sort( images )
table.sort( files )
table.sort( dirs )
local stuff = {
files = files,
dirs = dirs,
images = images
}
return stuff
end
utils['these_latest'] = function()
-- list last 10 modified files in our directory
local latest_path, latest_name = {}, {}
for i, file_path in ipairs( utils.latest_files( data_dir ) ) do
file_path = file_path:gsub( data_dir, "/" )
table.insert( latest_path, file_path)
-- local to_json = require("lapis.util").to_json
-- print( to_json(latest_path) )
local temp = ""
local result = ""
for i = file_path:len(), 1, -1 do
if file_path:sub(i,i) ~= "/" then
temp = temp..file_path:sub(i,i)
else
break
end
end
for j = temp:len(), 1, -1 do
result = result..temp:sub(j,j)
end
table.insert( latest_name, result )
end
return latest_path, latest_name
end
utils['total_files_dir'] = function( path )
local i, t, popen = 0, {}, io.popen
local pfile = popen('find "'..path..'" -type f | wc -l')
for total in pfile:lines() do
t[i] = total
i = i + 1
end
pfile:close()
return t
end
return utils

83
code/mime.types Normal file
View File

@ -0,0 +1,83 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/x-lua lua;
application/x-moonscript moon;
application/x-javascript js;
application/atom+xml atom;
application/rss+xml rss;
application/json json;
application/x-bittorrent torrent;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
image/svg+xml svg svgz;
image/webp webp;
application/java-archive jar war ear;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.ms-excel xls;
application/vnd.ms-powerpoint ppt;
application/vnd.wap.wmlc wmlc;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream eot;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

45
code/nginx.conf Normal file
View File

@ -0,0 +1,45 @@
worker_processes ${{NUM_WORKERS}};
error_log stderr notice;
daemon off;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
resolver 127.0.0.11;
init_by_lua_block {
require("socket")
require("lpeg")
math.randomseed(os.time()+ngx.worker.id())
}
server {
listen ${{PORT}};
lua_code_cache ${{CODE_CACHE}};
add_header Access-Control-Allow-Origin *;
location /data/ {
root /mnt;
}
location /static/ {
alias static/;
}
location / {
default_type text/html;
content_by_lua_block {
require("lapis").serve("app")
}
}
location /favicon.ico {
alias static/favicon.ico;
}
}
}

410
code/static/style.css Normal file
View File

@ -0,0 +1,410 @@
.h1
{
font-family: Staatliches;
color: #e2001a;
border-top: 3px solid #e2001a;
border-bottom: 3px solid#e2001a;
padding-top: 5px;
padding-bottom: 5px;
margin-bottom: 15px;
margin-top: 0px;
line-height: 1em;
}
.p
{
font-family: Helvetica;
color: #7e7e7e;
margin-bottom: 45px;
font-size: 10px;
}
.href
{
font-family: Helvetica;
text-decoration: none;
}
.mixlink
{
line-height: 16px;
font-weight: 600;
font-size: 14px;
margin-bottom: 0;
margin-top: 0;
font-family: Helvetica, Arial, sans-serif;
}
.amixlink
{
font-family: Helvetica;
color: #9d9d9d;
line-height: 1.6em;
font-weight: 600;
font-size: 14px;
text-decoration: none;
background: #111111;
display: inline-flex;
margin-top: 2px;
margin-bottom: 2px;
padding: 2px 10px;
border-radius: 20px;
}
.amixlink:hover
{
font-family: Helvetica;
color: #000;
line-height: 1.6em;
font-weight: 600;
font-size: 14px;
background: #e2001a;
}
.body
{
width: 80%;
padding-left: 10%;
padding-right: 10%;
background-color: #222222;
}
.mixsections
{
font-family: Staatliches;
font-weight: 900;
font-size: 16px;
color: #787878;
padding: 5px;
margin-top: 2%;
margin-bottom: 5px;
margin-right: 15px;
border: solid 3px #787878;
text-decoration: none;
}
.hotmixlogo
{
width: 150px;
padding-bottom: 0px;
padding-left: 10px;
padding-top: 10px;
}
.h2
{
font-family: Staatliches;
font-size: 24px;
color: #4c4c4c;
font-weight: 200;
border-bottom: 2px solid #4c4c4c;
margin-bottom: 0px;
}
.header
{
width: 100%;
background-image: url('/img/header.png');
max-height: 185px;
}
.footer
{
width: 100%;
height: 250px;
border-top: 2px solid #e2001a;
float: left;
clear: both;
}
.djsection
{
font-family: Staatliches;
font-size: 18px;
text-decoration: inherit;
color: #e2001a;
}
.djsectionmenu
{
font-family: Staatliches;
font-size: 33px;
color: #e2001a;
line-height: 1em;
z-index: 100;
display: block;
width: 10%;
height: 130px;
float: left;
margin-top: 5px;
margin-bottom: 5px;
min-width: 130px;
}
.djsectionmenu:hover
{
background-color: #e3301a;
}
.djsection
{
font-family: Staatliches;
font-size: 33px;
color: #e2001a;
line-height: 1em;
margin-bottom: 0px;
margin-top: 0px;
padding-top: 5px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 5px;
}
.djsection:hover
{
background-color: #000;
}
.h2
{
font-family: Staatliches;
color: #e2001a;
font-size: 24px;
padding-top: 5px;
border-bottom: 2px solid #e2001a;
}
.homeheader
{
background-image: url('/img/header.png');
background-repeat: no-repeat;
padding-bottom: 20px;
}
.bottomimg
{
}
.imglink
{
width: 200px;
}
.footerlegal
{
right: 0;
float: right;
margin-top: 80px;
}
/* Three image containers (use 25% for four, and 50% for two, etc) */
.column
{
float: left;
margin-left: 0px;
padding-left: 2.5%;
padding-right: 2.5%;
width: 15%;
}
/* Clear floats after image containers */
.row::after
{
content: "";
clear: both;
display: table;
}
@media screen and (max-width: 768px)
{
.column
{
padding-left: 5%;
padding-right: 5%;
width: 40%;
}
.h2
{
font-size: 14px;
}
.footer
{
margin-top: 15%;
}
}
@media screen and (max-width: 500px)
{
.column
{
padding-left: 0%;
padding-right: 5%;
width: 45%;
}
}
.profileimg
{
overflow: hidden;
height: auto;
width: 30%;
}
.profilesection
{
width: 100%;
overflow: hidden;
margin-bottom: 2.5%;
background-color: #e2001a;
}
.image_collage
{
line-height: 0;
-webkit-column-count: 5;
-webkit-column-gap: 0px;
-moz-column-count: 5;
-moz-column-gap: 0px;
column-count: 5;
column-gap: 0px;
}
.image_collage img
{
width: 100% !important;
height: auto !important;
}
@media (max-width: 1200px)
{
.image_collage
{
-moz-column-count: 4;
-webkit-column-count: 4;
column-count: 4;
}
}
@media (max-width: 1000px)
{
.image_collage
{
-moz-column-count: 3;
-webkit-column-count: 3;
column-count: 3;
}
}
@media (max-width: 800px)
{
.image_collage
{
-moz-column-count: 2;
-webkit-column-count: 2;
column-count: 2;
}
}
@media (max-width: 400px)
{
.image_collage
{
-moz-column-count: 1;
-webkit-column-count: 1;
column-count: 1;
}
}
.alphabetpanel
{
width: 100%;
float: left;
border-top: 1px dotted;
background-repeat: repeat;
overflow: hidden;
height: 100%;
}
@media (min-width:500px)
{
.alphabetimg
{
position: relative;
float: none;
top: 5px;
left: 5px;
height: 100px;
}
}
.alphabetgrid
{
width: 100%;
margin-top: 40px;
}
.latest
{
position: relative;
clear: both;
}
.latestdiv
{
width: 100%;
position: relative;
clear: both;
float: left;
}
.alphabet
{
width: 100%;
margin-bottom: 40px;
float: left;
clear: both;
margin-top: 20px;
}
.sponsors
{
margin-top: 20px;
clear: both;
float: left;
}
.linkimg:hover
{
filter: invert(70%);
}
@media (max-width:499px)
{
.djsectionmenu500
{
height: 70px;
overflow: hidden;
min-width: 60px;
width: 50%;
}
.alphabetimg500
{
height: 100px;
}
.alphabetpanel480down
{
height: 70px;
overflow: hidden;
width: 100px;
}
}
@media (max-width:767px)
{
.h1mobile
{
font-size: 18px;
}
}

35
code/views/layout.etlua Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title><%= titles['name'] %>
<% if title then %>
- <%= title %>
<% end %>
</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/style.css">
</head>
<body class="body">
<h1 class="h1 h1mobile">Tons Of Mix - Downloads & Streams (right click + Save link as... for downloads)<br>
<%= titles.name %><br>
Sharing <%= total[0] %> files</h1>
<% content_for("inner") %>
<section class="latest">
<div class="latest_files">
<br>
<br>
<h1 class="h1">Latest uploads:</h1>
<% for i, file in ipairs(latestpath) do %>
<a class="amixlink" href="<%= '/data' .. file %>">
<span class="mixlink"><%= latestname[i] %></span>
</a>
</br>
<% end %>
</div>
</section>
</body>
</html>

24
code/views/root.etlua Normal file
View File

@ -0,0 +1,24 @@
<div class="all_files">
<div class="image_collage">
<% for i, image in ipairs(images) do %>
<a href="<%= path .. image %>">
<img src="<%= path .. image %>">
</a>
<% end %>
</div>
<% for i, dir in ipairs(dirs) do %>
<br>
<a href="<%= uri .. dir %>" class="djsection"><span><%= dir %></span></a>
<br>
<% end %>
<br>
<% for i, file in ipairs(files) do %>
<a class="amixlink" href="<%= path .. file %>">
<span class="mixlink"><%= file %></span>
</a>
<br>
<% end %>
</div>

16
docker-compose-local.yaml Normal file
View File

@ -0,0 +1,16 @@
services:
videohotmix:
environment:
- ENVIRONMENT
build: .
networks:
- web
volumes:
- ./code:/srv/lapis
- ./_data:/mnt/data
ports:
- "8083:8080"
networks:
web:
external: true

23
docker-compose.yaml Normal file
View File

@ -0,0 +1,23 @@
services:
videohotmix:
build: .
networks:
- web
volumes:
- /data:/mnt/data:ro
labels:
- traefik.enable=true
- traefik.http.routers.hotmixes.rule=Host(`videohotmix.net`) || Host(`www.videohotmix.net`) || Host(`panamaracing.club`)
- traefik.http.routers.hotmixes.entrypoints=web
- traefik.http.routers.hotmixes.middlewares=redirect-https-hotmixes
- traefik.http.middlewares.redirect-https-hotmixes.redirectscheme.scheme=https
- traefik.http.routers.hotmixes_ssl.rule=Host(`videohotmix.net`) || Host(`www.videohotmix.net`) || Host(`panamaracing.club`)
- traefik.http.routers.hotmixes_ssl.entrypoints=websecure
- traefik.http.routers.hotmixes_ssl.tls.certresolver=myresolver
- traefik.http.services.hotmixes.loadbalancer.server.port=80
networks:
web:
external: true

18
videohotmix.service Normal file
View File

@ -0,0 +1,18 @@
[Unit]
Description=Videohotmix service with docker compose
Requires=docker.service
After=docker.service
[Service]
User=robot
Restart=always
WorkingDirectory=/home/robot/Sources/hotmixes.lapis
# Compose up
ExecStart=/usr/bin/docker compose up --build
# Compose down, remove containers
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target