From e8957c5e26996e1b7b5027824f30972f39f40c0f Mon Sep 17 00:00:00 2001 From: Carlo Landmeter Date: Thu, 14 Apr 2016 23:49:34 +0000 Subject: [PATCH] this is an extensive rewrite of the original code this should fix a following issues * run database updates from cron (its own process) * no more locked databases because of previous fix * have both edge and stable branches included * better handling of deps and revdeps * have a dedicated flagged section to see which pkgs are flagged * add support for a caching reverse proxy (see proxy config options) * and much more smaller changes... --- .gitignore | 2 +- aports.lua | 753 ++++++----------------------------- assets/bootstrap-chosen.css | 346 ++++++++++++++++ assets/chosen-sprite.png | Bin 0 -> 646 bytes assets/chosen-sprite@2x.png | Bin 0 -> 872 bytes assets/chosen.bootstrap.css | 461 +++++++++++++++++++++ assets/robots.txt | 2 - assets/style.css | 5 + conf.lua.default | 33 -- config.sample.lua | 76 ++++ controller.lua | 274 +++++++++++++ db.lua | 577 ++++++++++----------------- mail.lua | 36 +- model.lua | 234 +++++++++++ tools/generate_filelist.lua | 111 ------ tools/import.lua | 393 ++++++++++++++++++ tpl/contents.tpl | 72 ---- tpl/flagged.tpl | 61 --- views/contents.tpl | 91 +++++ {tpl => views}/flag.tpl | 29 +- views/flagged.tpl | 98 +++++ {tpl => views}/footer.tpl | 2 +- {tpl => views}/header.tpl | 4 +- {tpl => views}/mail_body.tpl | 0 {tpl => views}/package.tpl | 44 +- {tpl => views}/packages.tpl | 54 +-- 26 files changed, 2407 insertions(+), 1351 deletions(-) mode change 100755 => 100644 aports.lua create mode 100644 assets/bootstrap-chosen.css create mode 100644 assets/chosen-sprite.png create mode 100644 assets/chosen-sprite@2x.png create mode 100644 assets/chosen.bootstrap.css delete mode 100644 assets/robots.txt delete mode 100644 conf.lua.default create mode 100644 config.sample.lua create mode 100644 controller.lua create mode 100644 model.lua delete mode 100755 tools/generate_filelist.lua create mode 100644 tools/import.lua delete mode 100644 tpl/contents.tpl delete mode 100644 tpl/flagged.tpl create mode 100644 views/contents.tpl rename {tpl => views}/flag.tpl (65%) create mode 100644 views/flagged.tpl rename {tpl => views}/footer.tpl (81%) rename {tpl => views}/header.tpl (93%) rename {tpl => views}/mail_body.tpl (100%) rename {tpl => views}/package.tpl (75%) rename {tpl => views}/packages.tpl (57%) diff --git a/.gitignore b/.gitignore index 2fc70a3..338c30b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -conf.lua +config.lua diff --git a/aports.lua b/aports.lua old mode 100755 new mode 100644 index f5287c1..0ce4e62 --- a/aports.lua +++ b/aports.lua @@ -1,674 +1,179 @@ #!/usr/bin/env luajit --- include config file -conf = dofile("conf.lua") +TURBO_SSL = true +turbo = require("turbo") +conf = require("config") +cntrl = require("controller") +model = require("model") +db = require("db") --- lua turbo application -TURBO_SSL = true -turbo = require "turbo" +inspect = require("inspect") -local smtp = require("socket.smtp") --- we use lustache instead of turbo's limited mustache engine -local lustache = require("lustache") - -local db = require("db") -local apkindex = db.apkindex() -local filelist = db.filelist() -local flagged = db.flagged() - -local mail = require("mail") - --- --- usefule lua helper functions --- - --- check if string begins with prefix -function string.begins(str, prefix) - return str:sub(1,#prefix)==prefix -end - --- urlencode a string -function urlencode(str) - if (str) then - str = string.gsub (str, "\n", "\r\n") - str = string.gsub (str, "([^%w ])", - function (c) return string.format ("%%%02X", string.byte(c)) end) - str = string.gsub (str, " ", "+") - end - return str -end - --- convert bytes to human readable -function human_bytes(bytes) - local mult = 10^(2) - local size = { 'B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' } - local factor = math.floor((string.len(bytes) -1) /3) - local result = bytes/math.pow(1024, factor) - return math.floor(result * mult + 0.5) / mult.." "..size[factor+1] -end - --- return a filtered valid email address or nil -function validate_email(addr) - return addr:match("[A-Za-z0-9%.%%%+%-]+@[A-Za-z0-9%.%%%+%-]+%.%w%w%w?%w?") -end - -function parse_rfc822(addr) - local r = {} - if is_set(addr) then - local name, email = turbo.escape.trim(addr):match("(.*)(<.*>)") - if is_set(email) then r.email = validate_email(email) end - if is_set(name) then r.name = turbo.escape.trim(name) end - end - if r.email or r.name then return r end -end - -function format_maintainer(maintainer) - local addr = parse_rfc822(maintainer) - if addr and addr.name then - return addr.name - else - return "None" - end -end - -function is_set(str) - if str and str ~= "" then return str end -end - --- read the tpl file into a string and return it -function tpl(tpl) - local f = io.open(conf.tpl.."/"..tpl, "rb") - local r = f:read("*all") - f:close() - return r -end - -function create_db() - apkindex:create() - filelist:create() -end - --- format a date -function format_date(ts) - return os.date('%Y-%m-%d %H:%M:%S', ts) -end - --- --- Provide alert messages to mustache --- -local Alert = class("Alert") - -function Alert:initialize() - self.alert = false -end - -function Alert:set_msg(msg, type) - self.alert = {{msg=msg,type=type}} -end +-- packages renderer to display a list of packages +local packagesRenderer = class("packagesRenderer", turbo.web.RequestHandler) -function Alert:get_msg() - local r = self.alert - self.alert = false - return r +function packagesRenderer:prepare() + db:open() end --- --- misc functions --- - --- check if version exist in apkindex -function OriginExists(repo, origin, version) - local origins = apkindex:get_origin(repo, origin) - if (type(origins) == "table") then - for _,f in pairs (origins) do - if f.version == version then - return true - end - end - end +function packagesRenderer:on_finish() + db:close() end --- send the responce back to google to verify the captcha -function RecaptchaVerify(response) - local uri = "https://www.google.com/recaptcha/api/siteverify" - local kwargs = {} - kwargs.method = "POST" - kwargs.params = { - secret = conf.rc.secret, - response = response +function packagesRenderer:get() + local args = { + name = self:get_argument("name","", true), + branch = self:get_argument("branch", "", true), + arch = self:get_argument("arch", "", true), + repo = self:get_argument("repo", "", true), + maintainer = self:get_argument("maintainer", "", true), + page = tonumber(self:get_argument("page", 1, true)) or 1, } - local res = coroutine.yield(turbo.async.HTTPClient():fetch(uri, kwargs)) - if res.error then - return false - end - local result = turbo.escape.json_decode(res.body) - return result.success + self:write(cntrl:packages(args)) end --- simple helper to filter our deps in case we find multiple dep packages --- in multiple repositories, deps in its own repo are always prefered -function FilterDeps(deps, repo) - for k,v in pairs(deps) do - if (v.repo == repo) then - return v - end - end - return deps[1] -end - --- create a array with pager page numbers --- append is last page in the pager -function CreatePager(total, limit, current, offset) - local r = {} - local pages = math.ceil(total/limit) - local pagers = math.min(offset*2+1, pages) - local first = math.max(1, current - offset) - local last = math.min(pages, (pagers + first - 1)) - local size = (last-first+1) - if size < pagers then - first = first - (pagers - size) - end - for p = first, last do - table.insert(r, p) - end - r.last = pages - return r -end - --- --- Model section --- +-- package renderer, to display a single package +local packageRenderer = class("packageRenderer", turbo.web.RequestHandler) -function PagerModel(args, total) - local result = {} - local pager = CreatePager(total, conf.pager.limit, args.page, conf.pager.offset) - if pager.last > conf.pager.offset then - table.insert(pager, 1, "«") - table.insert(pager, "»") - for k,p in ipairs(pager) do - local r = {} - for g,v in pairs(args) do - if (g=="page") then - if p == "«" then - v=1 - elseif p == "»" then - v=pager.last - else - v=p - end - end - r[#r+1]=string.format("%s=%s", urlencode(g), urlencode(v)) - end - path=table.concat(r, '&') - class = (args.page == p) and "active" or "" - table.insert(result, {args=path, class=class, page=p}) - end - end - return result +function packageRenderer:prepare() + db:open() end --- get pkgname with same origin -function SubPackagesModel(pkg) - local r = {} - local origins = apkindex:get_origin(pkg.repo, pkg.origin, pkg.arch) - for k,v in pairs (origins) do - if v.name ~= pkg.name then - r[#r+1] = { - path=string.format("/package/%s/%s/%s", v.repo, v.arch, v.name), - text=v.name - } - end - end - return r +function packageRenderer:on_finish() + db:close() end -function DependsModel(pkg) - local r,d,sd,rd = {},{},{},{} - for k,v in pairs (pkg.depends:split(" ")) do - -- resolve so deps - if v:begins('so:') then - sdpkg = apkindex:get_depends("%"..v.."%", "%", pkg.arch) - if next(sdpkg) then - sd = FilterDeps(sdpkg, pkg.repo) - r[sd.name] = sd - end - else - local name = v:gsub('=.*', '') - dpkg = apkindex:get_depends("%", name, pkg.arch) - if next(dpkg) then - d = FilterDeps(dpkg, pkg.repo) - r[d.name] = d - end - end - end - for name,p in pairs(r) do - rd[#rd+1] = { - path=string.format("/package/%s/%s/%s", p.repo, p.arch, p.name), - text=p.name - } - end - return rd -end - -function RequiredByModel(pkg) - local pkgs,r,rb = {},{},{} - local deps = pkg.provides:split(" ") - for _,d in pairs(deps) do - -- remove version data - -- we do not support verioned deps - d = d:gsub('=.*', '') - pkgs = apkindex:get_required_by("%"..d.."%", pkg.arch) - for k,v in pairs(pkgs) do - r[v.name..v.repo] = v - end - end - -- lookup package that depends on pkgname - pkgs = apkindex:get_required_by("%"..pkg.name.."%", pkg.arch) - for k,v in pairs(pkgs) do - if turbo.util.is_in(pkg.name, v.depends:split(" ")) then - r[v.name..v.repo] = v - end - end - -- format the model - for _,p in pairs(r) do - rb[#rb+1] = { - path=string.format("/package/%s/%s/%s", p.repo, p.arch, p.name), - text=p.name - } - end - return rb -end - -function PackageModel(pkg) - local flag = flagged:get_status(pkg.origin, pkg.repo, pkg.version) - if flag then - pkg.version = { - class="text-danger", - text=pkg.version, - title=string.format("Flagged: %s", format_date(flag.date)), - path="#" - } +function packageRenderer:get(branch, repo, arch, name) + local ops = {branch=branch,repo=repo,arch=arch, name=name} + local pkg = cntrl:getPackage(ops) + if next(pkg) then + self:write(cntrl:package(pkg)) else - pkg.version = { - class="text-success", - text=pkg.version, - title=string.format("Flag this package out of date"), - path=string.format("/flag/%s/%s/%s", pkg.repo, pkg.origin, pkg.version), - } - end - return { - name = is_set(pkg.name) or "None", - version = is_set(pkg.version) or "None", - description = is_set(pkg.description) or "None", - url = is_set(pkg.url) or "None", - license = is_set(pkg.license) or "None", - repo = is_set(pkg.repo) or "None", - arch = is_set(pkg.arch) or "None", - size = pkg.size and human_bytes(pkg.size) or "None", - installed_size = is_set(pkg.installed_size) and human_bytes(pkg.installed_size) or "None", - origin = is_set(pkg.origin) and { - path=string.format("/package/%s/%s/%s", pkg.repo, pkg.arch, pkg.origin), - text=pkg.origin - } or {path="#", text="None"}, - maintainer = is_set(format_maintainer(pkg.maintainer)) or "None", - build_time = is_set(pkg.build_time) and format_date(pkg.build_time) or "None", - commit = is_set(pkg.commit) and { - path=string.format("http://git.alpinelinux.org/cgit/aports/commit/?id=%s", pkg.commit), - text=pkg.commit - } or {path="#", text="None"}, - contents = { - path=string.format("/contents?pkgname=%s&arch=%s&repo=%s", pkg.name, pkg.arch, pkg.repo), - text="Contents of package" - }, - } -end - -function PackagesModel(pkgs) - local r = {} - for k,v in pairs(pkgs) do - r[k] = {} - r[k].name = { - path=string.format("/package/%s/%s/%s", v.repo, v.arch, v.name), - text=v.name, - title=v.description - } - r[k].version = { - path=string.format("/flag/%s/%s/%s", v.repo, v.origin, v.version), - text=v.version, - title="Flag this package out of date" - } - r[k].url = { - path=v.url, - text="URL", - title=v.url - } - r[k].license = v.license - r[k].arch = v.arch - r[k].repo = v.repo - r[k].maintainer = format_maintainer(v.maintainer) - r[k].build_time = format_date(v.build_time) - local fs = flagged:get_status(v.origin, v.repo, v.version) - if (fs) then r[k].flagged = {date=format_date(fs.date)} end - end - return r -end - -function ContentsModel(contents) - local r = {} - for k,v in pairs(contents) do - r[k] = {} - r[k].file = string.format("/%s/%s", v.path, v.file) - r[k].pkgname = { - path = string.format("/package/%s/%s/%s", v.repo, v.arch, v.pkgname), - text = v.pkgname, - } - r[k].repo = v.repo - r[k].arch = v.arch - end - return r -end - -function FormArchModel(selected) - local r = {} - local archs = apkindex:get_distinct("arch") - table.insert(archs, {arch="all"}) - for k,v in pairs(archs) do - r[k] = {text=v.arch} - if (v.arch == selected) then - r[k].selected = "selected" - end - end - return r -end - -function FormRepoModel(selected) - local r = {} - local repos = apkindex:get_distinct("repo") - table.insert(repos, {repo="all"}) - for k,v in pairs(repos) do - r[k] = {text=v.repo} - if (v.repo == selected) then - r[k].selected = "selected" - end + error(turbo.web.HTTPError(404, "404 Page not found.")) end - return r end -function FormMaintainerModel(selected) - local r = {} - -- create a list of maintainers by name and filter dups - local maintainers = apkindex:get_distinct("maintainer") - for k,v in ipairs(maintainers) do - local m = parse_rfc822(v.maintainer) - if m and is_set(m.name) then - r[m.name] = m.name - end - end - -- sort the table - local s = {} - for k,v in pairs(r) do - table.insert(s,v) - end - table.sort(s) - -- create the model - local t = {{value="all", text="all"}} - if selected == "all" then t[#t].selected = "selected" end - for k,v in ipairs(s) do - t[#t+1] = {value=v,text=limit_string(v, 25)} - if v == selected then t[#t].selected = "selected" end - end - return t -end +local contentsRenderer = class("contentsRenderer", turbo.web.RequestHandler) -function limit_string(str, len) - if string.len(str) > len then - return string.sub(str,1,len).."..." - end - return str +function contentsRenderer:prepare() + db:open() end -function PackagesFormModel(name, repo, arch, maintainer) - local r = {} - r.repo = FormRepoModel(repo) - r.arch = FormArchModel(arch) - r.maintainer = FormMaintainerModel(maintainer) - r.name = name - return r +function contentsRenderer:on_finish() + db:close() end -function ContentsFormModel(file, path, pkgname, repo, arch) - return { - repo = FormRepoModel(repo), - arch = FormArchModel(arch), - filename = file, - path = path, - name = pkgname, +function contentsRenderer:get() + local args = { + file = self:get_argument("file", "", true), + path = self:get_argument("path", "", true), + name = self:get_argument("name", "", true), + branch = self:get_argument("branch", "", true), + repo = self:get_argument("repo", "", true), + arch = self:get_argument("arch", "", true), + page = tonumber(self:get_argument("page", 1, true)) or 1, } + self:write(cntrl:contents(args)) end -function FlagModel(pkg) - return { - repo = pkg.repo, - origin = pkg.origin, - version = pkg.version, - maintainer = format_maintainer(pkg.maintainer), - sitekey = conf.rc.sitekey - } -end +-- flagged renderer, to display the flag form +local flagRenderer = class("flagRenderer", turbo.web.RequestHandler) -function FlaggedModel(pkgs, arch) - local r = {} - for k,v in ipairs(pkgs) do - r[k] = {} - r[k].origin = { - text = v.origin, - path = string.format("/packages?name=%s&arch=%s", v.origin, arch), - title = "", - } - r[k].repo = v.repo - r[k].version = v.version - r[k].date = format_date(v.date) - r[k].message = v.message - end - return r +function flagRenderer:prepare() + db:open() end -function MailMaintainerModel(pkg, from, message) - return { - maintainer = format_maintainer(pkg.maintainer), - origin = pkg.origin, - from = from, - message = message - } +function flagRenderer:on_finish() + db:close() end --- --- Turbo request handlers --- - --- contentets renderer to display package contents -local ContentsRenderer = class("ContentsRenderer", turbo.web.RequestHandler) -function ContentsRenderer:get() - local m = {} - local a = { - filename = self:get_argument("filename", "", true), - path = self:get_argument("path", "", true), - pkgname = self:get_argument("pkgname", "", true), - arch = self:get_argument("arch", "x86_64", true), - repo = self:get_argument("repo", "all", true), - page = tonumber(self:get_argument("page", 1, true)) - } - m.nav = {package="", content="active"} - m.alert = self.options.alert:get_msg() - m.form = ContentsFormModel(a.filename, a.path, a.pkgname, a.repo, a.arch) - if not (a.filename == "" and a.path == "" and a.pkgname == "") then - local contents = filelist:get_files(a.filename, a.path, a.pkgname, a.arch, a.repo, a.page) - local count = filelist:count_files(a.filename, a.path, a.pkgname, a.arch, a.repo) - m.contents = ContentsModel(contents) - m.pager = PagerModel(a, count) +function flagRenderer:get(branch, repo, origin, version) + local ops = {branch=branch, repo=repo, origin=origin, version=version} + local page = cntrl:flag(ops) + local pkg = cntrl:getNotFlagged(ops) + if pkg then + self:write(cntrl:flag(pkg)) + else + error(turbo.web.HTTPError(404, "404 Page not found.")) end - m.header = lustache:render(tpl("header.tpl"), m) - m.footer = lustache:render(tpl("footer.tpl"), m) - self:write(lustache:render(tpl("contents.tpl"), m)) end --- packages renderer to display a list of packages -local PackagesRenderer = class("PackagesRenderer", turbo.web.RequestHandler) - -function PackagesRenderer:get() +function flagRenderer:post(branch, repo, origin, version) local m = {} - local a = { - name = self:get_argument("name","", true), - arch = self:get_argument("arch", "x86_64", true), - repo = self:get_argument("repo", "all", true), - maintainer = self:get_argument("maintainer", "all", true), - page = tonumber(self:get_argument("page", 1, true)), + local args = { + from = self:get_argument("from", "", true), + new_version = self:get_argument("new_version", "", true), + message = self:get_argument("message", "", true), + responce = self:get_argument("g-recaptcha-response", "", true), } - local pkgs = apkindex:get_packages(a.name, a.repo, a.arch, a.maintainer, a.page) - local num = apkindex:count_packages(a.name, a.repo, a.arch, a.maintainer) - m.nav = {package="active", content=""} - m.alert = self.options.alert:get_msg() - m.form = PackagesFormModel(a.name, a.repo, a.arch, a.maintainer) - m.pkgs = PackagesModel(pkgs) - m.pager = PagerModel(a, num) - m.header = lustache:render(tpl("header.tpl"), m) - m.footer = lustache:render(tpl("footer.tpl"), m) - self:write(lustache:render(tpl("packages.tpl"), m)) -end - --- package renderer, to display a single package -local PackageRenderer = class("PackageRenderer", turbo.web.RequestHandler) - -function PackageRenderer:get(repo, arch, name) - local m = {} - local ops = {repo=repo, arch=arch, name=name} - local pkg = apkindex:get_package(ops) + local ops = { branch=branch, repo=repo, name=origin, version=version } + m.form = {value=args} + local pkg = cntrl:getNotFlagged(ops) + -- check if pkg exists and is not flagged if not pkg then error(turbo.web.HTTPError(404, "404 Page not found.")) - end - m.nav = {package="active", content=""} - m.alert = self.options.alert:get_msg() - m.pkg = PackageModel(pkg) - m.deps = DependsModel(pkg) - m.deps_qty = (m.deps ~= nil) and #m.deps or "0" - m.subpkgs = SubPackagesModel(pkg) - m.subpkgs_qty = (m.subpkgs) and #m.subpkgs or "0" - m.reqbys = RequiredByModel(pkg) - m.reqbys_qty = (m.reqbys ~= nil) and #m.reqbys or "0" - m.header = lustache:render(tpl("header.tpl"), m) - m.footer = lustache:render(tpl("footer.tpl"), m) - self:write(lustache:render(tpl("package.tpl"), m)) -end - --- flagged renderer, to display the flag form -local FlagRenderer = class("FlagRenderer", turbo.web.RequestHandler) - -function FlagRenderer:get(repo, origin, version) - local args = {"flag",repo,origin,version} - local ops = {origin=origin, repo=repo} - local pkg = apkindex:get_package(ops) - --trow an http error if this package doesnt exists - if not OriginExists(repo, origin, version) then - error(turbo.web.HTTPError(404, "404 Page not found.")) - -- display alert when origin is already flagged - elseif flagged:get_status(origin, repo, version) then - alert = "This origin has already been flagged" - self.options.alert:set_msg(alert, "danger") - end - local m = FlagModel(pkg) - m.alert = self.options.alert:get_msg() - m.header = lustache:render(tpl("header.tpl"), m) - m.footer = lustache:render(tpl("footer.tpl"), m) - self:write(lustache:render(tpl("flag.tpl"), m)) -end - --- flagged post function when submitting the form -function FlagRenderer:post(repo, origin, version) - local alert = "Sucessfully flagged packages" - local type = "danger" - local args = {"flag",repo,origin,version} - local message = self:get_argument("message", false) - local from = self:get_argument("from", "") - local responce = self:get_argument("g-recaptcha-response", "") - --- lets do some checks if this is a proper post - --trow an http error if this package doesnt exists - if not OriginExists(repo, origin, version) then - error(turbo.web.HTTPError(404, "404 Page not found.")) - -- display alert when origin is already flagged - elseif flagged:get_status(origin, repo, version) then - alert = "This origin has already been flagged" - -- check for valid email address - elseif not validate_email(from) then - alert = "Please provide a valid email address." - -- verify captha if enabled - elseif conf.rc.sitekey and not RecaptchaVerify(responce) then - alert = "Failed to pass recaptcha. Please try again." - -- flag origin, if failed we display an alert - elseif not flagged:flag_origin(repo, origin, version, message) then - alert = [[ Failed to flag origin package: origin ]] - -- all is well (we presume) + -- check if email is valid + elseif not cntrl:validateEmail(args.from) then + m.form.status = {from="has-error"} + m.alert = {type="danger",msg="Please provide a valid email address"} + self:write(cntrl:flag(pkg, m)) + -- check if new version is provided + elseif args.new_version == "" then + m.form.status = {new_version="has-error"} + m.alert = {type="danger",msg="Please provide a new upstream version number"} + self:write(cntrl:flag(pkg, m)) + -- check if message is provided + elseif args.message == "" then + m.form.status = {message="has-error"} + m.alert = {type="danger",msg="Please provide a message"} + self:write(cntrl:flag(pkg, m)) + -- check if recaptcha is correct + elseif conf.rc.sitekey and not cntrl:verifyRecaptcha(args.responce) then + m.alert = {type="danger",msg="reCAPTCHA failed, please try again"} + self:write(cntrl:flag(pkg, m)) + -- try to flag the package + elseif not db:flagOrigin(args, pkg) then + m.alert = {type="danger",msg="Could not flag package, please try again"} + self:write(cntrl:flag(pkg, m)) + -- successfully flagged, lets display the flagged origin package else - args = {"packages"} - type = "success" - local ops = {origin=origin, repo=repo} - local pkg = apkindex:get_package(ops) - -- Check if we have a valid maintainer recipient - if validate_email(pkg.maintainer) then - local subject = string.format("Alpine aport %s has been flagged out of date", origin) - mail:initialize() - mail:set_from(conf.mail.from) - mail:set_rcpt(pkg.maintainer) - mail:set_to(pkg.maintainer) - mail:set_subject(subject) - local model = MailMaintainerModel(pkg, from, message) - body = lustache:render(tpl("mail_body.tpl"), model) - mail:set_body(body) - local result = mail:send() - if not result then - alert = "Succesfully notified maintainer" - type = "success" - end - end + cntrl:flagMail(args, pkg) + m.alert = {type="success",msg="Succesfully flagged package"} + self:write(cntrl:package(pkg, m)) end - -- set the alert and redirect - self.options.alert:set_msg(alert, type) - self:redirect("/"..table.concat(args, "/"), true) end --- flagged renderer, to display the flag form -local FlaggedRenderer = class("FlaggedRenderer", turbo.web.RequestHandler) +local flaggedRenderer = class("flaggedRenderer", turbo.web.RequestHandler) -function FlaggedRenderer:get() - local m = {} - local name = self:get_argument("name","", true) - local arch = self:get_argument("arch", "x86_64", true) - local repo = self:get_argument("repo", "all", true) - local page = tonumber(self:get_argument("page", 1, true)) - m.form = PackagesFormModel(name, repo, arch) - local ops = {origin=name,repo=repo} - local pkgs = flagged:get_flagged(ops) - m.pkgs = FlaggedModel(pkgs, arch) - m.header = lustache:render(tpl("header.tpl"), m) - m.footer = lustache:render(tpl("footer.tpl"), m) - self:write(lustache:render(tpl("flagged.tpl"), m)) +function flaggedRenderer:prepare() + db:open() end -function main() - local alert = Alert() - turbo.web.Application({ - {"^/$", turbo.web.RedirectHandler, "/packages"}, - {"^/contents$", ContentsRenderer, {alert=alert}}, - {"^/packages$", PackagesRenderer, {alert=alert}}, - {"^/package/(.*)/(.*)/(.*)$", PackageRenderer, {alert=alert}}, - {"^/flag/(.*)/(.*)/(.*)$", FlagRenderer, {alert=alert}}, - {"^/flagged$", FlaggedRenderer, {alert=alert}}, - {"^/assets/(.*)$", turbo.web.StaticFileHandler, "assets/"}, - {"^/robots.txt", turbo.web.StaticFileHandler, "assets/robots.txt"}, - }):listen(conf.port) - local loop = turbo.ioloop.instance() - loop:set_interval(60000 * conf.update, create_db) - loop:start() +function flaggedRenderer:on_finish() + db:close() end -create_db() -main() +function flaggedRenderer:get() + local args = { + name = self:get_argument("name","", true), + branch = self:get_argument("branch", "", true), + arch = self:get_argument("arch", "", true), + repo = self:get_argument("repo", "", true), + maintainer = self:get_argument("maintainer", "", true), + page = tonumber(self:get_argument("page", 1, true)) or 1, + } + self:write(cntrl:flagged(args)) +end + +turbo.web.Application({ + {"^/$", turbo.web.RedirectHandler, "/packages"}, + {"^/contents$", contentsRenderer}, + {"^/packages$", packagesRenderer}, + {"^/package/(.*)/(.*)/(.*)/(.*)$", packageRenderer}, + {"^/flag/(.*)/(.*)/(.*)/(.*)$", flagRenderer}, + {"^/flagged$", flaggedRenderer}, + {"^/assets/(.*)$", turbo.web.StaticFileHandler, "assets/"}, + {"^/robots.txt", turbo.web.StaticFileHandler, "assets/robots.txt"}, +}):listen(conf.port) +local loop = turbo.ioloop.instance() +loop:start() diff --git a/assets/bootstrap-chosen.css b/assets/bootstrap-chosen.css new file mode 100644 index 0000000..54e6a13 --- /dev/null +++ b/assets/bootstrap-chosen.css @@ -0,0 +1,346 @@ +.chosen-select { + width: 100%; } + +.chosen-select-deselect { + width: 100%; } + +.chosen-container { + display: inline-block; + font-size: 14px; + position: relative; + vertical-align: middle; } + .chosen-container .chosen-drop { + background: #fff; + border: 1px solid #ccc; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-box-shadow: 0 8px 8px rgba(0, 0, 0, 0.25); + box-shadow: 0 8px 8px rgba(0, 0, 0, 0.25); + margin-top: -1px; + position: absolute; + top: 100%; + left: -9000px; + z-index: 1060; } + .chosen-container.chosen-with-drop .chosen-drop { + left: 0; + right: 0; } + .chosen-container .chosen-results { + color: #555555; + margin: 0 4px 4px 0; + max-height: 240px; + padding: 0 0 0 4px; + position: relative; + overflow-x: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; } + .chosen-container .chosen-results li { + display: none; + line-height: 1.42857; + list-style: none; + margin: 0; + padding: 5px 6px; } + .chosen-container .chosen-results li em { + background: #feffde; + font-style: normal; } + .chosen-container .chosen-results li.group-result { + display: list-item; + cursor: default; + color: #999; + font-weight: bold; } + .chosen-container .chosen-results li.group-option { + padding-left: 15px; } + .chosen-container .chosen-results li.active-result { + cursor: pointer; + display: list-item; } + .chosen-container .chosen-results li.highlighted { + background-color: #337ab7; + background-image: none; + color: white; } + .chosen-container .chosen-results li.highlighted em { + background: transparent; } + .chosen-container .chosen-results li.disabled-result { + display: list-item; + color: #777777; } + .chosen-container .chosen-results .no-results { + background: #eeeeee; + display: list-item; } + .chosen-container .chosen-results-scroll { + background: white; + margin: 0 4px; + position: absolute; + text-align: center; + width: 321px; + z-index: 1; } + .chosen-container .chosen-results-scroll span { + display: inline-block; + height: 1.42857; + text-indent: -5000px; + width: 9px; } + .chosen-container .chosen-results-scroll-down { + bottom: 0; } + .chosen-container .chosen-results-scroll-down span { + background: url("chosen-sprite.png") no-repeat -4px -3px; } + .chosen-container .chosen-results-scroll-up span { + background: url("chosen-sprite.png") no-repeat -22px -3px; } + +.chosen-container-single .chosen-single { + background-color: #fff; + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; + border: 1px solid #ccc; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + color: #555555; + display: block; + height: 34px; + overflow: hidden; + line-height: 34px; + padding: 0 0 0 8px; + position: relative; + text-decoration: none; + white-space: nowrap; } + .chosen-container-single .chosen-single span { + display: block; + margin-right: 26px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + .chosen-container-single .chosen-single abbr { + background: url("chosen-sprite.png") right top no-repeat; + display: block; + font-size: 1px; + height: 10px; + position: absolute; + right: 26px; + top: 12px; + width: 12px; } + .chosen-container-single .chosen-single abbr:hover { + background-position: right -11px; } + .chosen-container-single .chosen-single.chosen-disabled .chosen-single abbr:hover { + background-position: right 2px; } + .chosen-container-single .chosen-single div { + display: block; + height: 100%; + position: absolute; + top: 0; + right: 0; + width: 18px; } + .chosen-container-single .chosen-single div b { + background: url("chosen-sprite.png") no-repeat 0 7px; + display: block; + height: 100%; + width: 100%; } +.chosen-container-single .chosen-default { + color: #777777; } +.chosen-container-single .chosen-search { + margin: 0; + padding: 3px 4px; + position: relative; + white-space: nowrap; + z-index: 1000; } + .chosen-container-single .chosen-search input[type="text"] { + background: url("chosen-sprite.png") no-repeat 100% -20px, #fff; + border: 1px solid #ccc; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + margin: 1px 0; + padding: 4px 20px 4px 4px; + width: 100%; } +.chosen-container-single .chosen-drop { + margin-top: -1px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; } + +.chosen-container-single-nosearch .chosen-search input[type="text"] { + position: absolute; + left: -9000px; } + +.chosen-container-multi .chosen-choices { + background-color: #fff; + border: 1px solid #ccc; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + cursor: text; + height: auto !important; + height: 1%; + margin: 0; + overflow: hidden; + padding: 0; + position: relative; } + .chosen-container-multi .chosen-choices li { + float: left; + list-style: none; } + .chosen-container-multi .chosen-choices .search-field { + margin: 0; + padding: 0; + white-space: nowrap; } + .chosen-container-multi .chosen-choices .search-field input[type="text"] { + background: transparent !important; + border: 0 !important; + -webkit-box-shadow: none; + box-shadow: none; + color: #555555; + height: 32px; + margin: 0; + padding: 4px; + outline: 0; } + .chosen-container-multi .chosen-choices .search-field .default { + color: #999; } + .chosen-container-multi .chosen-choices .search-choice { + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; + background-color: #eeeeee; + border: 1px solid #ccc; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 100%); + background-image: -o-linear-gradient(top, white 0%, #eeeeee 100%); + background-image: linear-gradient(to bottom, white 0%, #eeeeee 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + color: #333333; + cursor: default; + line-height: 13px; + margin: 6px 0 3px 5px; + padding: 3px 20px 3px 5px; + position: relative; } + .chosen-container-multi .chosen-choices .search-choice .search-choice-close { + background: url("chosen-sprite.png") right top no-repeat; + display: block; + font-size: 1px; + height: 10px; + position: absolute; + right: 4px; + top: 5px; + width: 12px; + cursor: pointer; } + .chosen-container-multi .chosen-choices .search-choice .search-choice-close:hover { + background-position: right -11px; } + .chosen-container-multi .chosen-choices .search-choice-focus { + background: #d4d4d4; } + .chosen-container-multi .chosen-choices .search-choice-focus .search-choice-close { + background-position: right -11px; } +.chosen-container-multi .chosen-results { + margin: 0 0 0 0; + padding: 0; } +.chosen-container-multi .chosen-drop .result-selected { + display: none; } + +.chosen-container-active .chosen-single { + border: 1px solid #66afe9; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px #66afe9; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px #66afe9; + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; } +.chosen-container-active.chosen-with-drop .chosen-single { + background-color: #fff; + border: 1px solid #66afe9; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px #66afe9; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px #66afe9; + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; } + .chosen-container-active.chosen-with-drop .chosen-single div { + background: transparent; + border-left: none; } + .chosen-container-active.chosen-with-drop .chosen-single div b { + background-position: -18px 7px; } +.chosen-container-active .chosen-choices { + border: 1px solid #66afe9; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px #66afe9; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px #66afe9; + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; } + .chosen-container-active .chosen-choices .search-field input[type="text"] { + color: #111 !important; } +.chosen-container-active.chosen-with-drop .chosen-choices { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } + +.chosen-disabled { + cursor: default; + opacity: 0.5 !important; } + .chosen-disabled .chosen-single { + cursor: default; } + .chosen-disabled .chosen-choices .search-choice .search-choice-close { + cursor: default; } + +.chosen-rtl { + text-align: right; } + .chosen-rtl .chosen-single { + padding: 0 8px 0 0; + overflow: visible; } + .chosen-rtl .chosen-single span { + margin-left: 26px; + margin-right: 0; + direction: rtl; } + .chosen-rtl .chosen-single div { + left: 7px; + right: auto; } + .chosen-rtl .chosen-single abbr { + left: 26px; + right: auto; } + .chosen-rtl .chosen-choices .search-field input[type="text"] { + direction: rtl; } + .chosen-rtl .chosen-choices li { + float: right; } + .chosen-rtl .chosen-choices .search-choice { + margin: 6px 5px 3px 0; + padding: 3px 5px 3px 19px; } + .chosen-rtl .chosen-choices .search-choice .search-choice-close { + background-position: right top; + left: 4px; + right: auto; } + .chosen-rtl.chosen-container-single .chosen-results { + margin: 0 0 4px 4px; + padding: 0 4px 0 0; } + .chosen-rtl .chosen-results .group-option { + padding-left: 0; + padding-right: 15px; } + .chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div { + border-right: none; } + .chosen-rtl .chosen-search input[type="text"] { + background: url("chosen-sprite.png") no-repeat -28px -20px, #fff; + direction: rtl; + padding: 4px 5px 4px 20px; } + +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-resolution: 2dppx) { + .chosen-rtl .chosen-search input[type="text"], + .chosen-container-single .chosen-single abbr, + .chosen-container-single .chosen-single div b, + .chosen-container-single .chosen-search input[type="text"], + .chosen-container-multi .chosen-choices .search-choice .search-choice-close, + .chosen-container .chosen-results-scroll-down span, + .chosen-container .chosen-results-scroll-up span { + background-image: url("chosen-sprite@2x.png") !important; + background-size: 52px 37px !important; + background-repeat: no-repeat !important; } } + +/*# sourceMappingURL=bootstrap-chosen.css.map */ diff --git a/assets/chosen-sprite.png b/assets/chosen-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..3611ae4ace1c4b1cbeacd6145b5a79cbc72e0bdc GIT binary patch literal 646 zcmV;10(t$3P)0006_Nkl&pbAC0n?)s%2x5M$#UgGxI1~gymp~v; zh<`zGaTJP5BybQY4tlRo;SIcmE0t>ueW3>*u6N;@_u;;|BoL8PuhZ#FWY9$(flan1oVvxDBL8~}0Q5z;^2p>Ov z7}Q$E7=l>$BZLUt1*uKMCaQwKYf$lsJCyerWMd+%BeGH6f_f&Vpy%=$BN%uK%Ahxi zF+%Jj=Pc-WXF;djSiJw|m4>cN%^Fi|FBZ!E{_yJLS?RI<3U696XLkQSs z{{&kQJ$K(#5CgC1;GbA>mjm56zJZ^-Hg2_ASKI_K@CybPh7Rq}8ud``)NM~eZx}qT z)oK?Lf>t!B;%Uh}*P+)Ef?jbx#w|BNEVb(l{2LK}m#fw4xx>ddF;&j}!S{N-e!<&Q z?kvzyI_AT@H-u4tUz1h&GCh8>?TnA~?BZ7WGP<9`r gQ6DuZO!_~60ju@{TzXF%TmS$707*qoM6N<$g2#CqL;wH) literal 0 HcmV?d00001 diff --git a/assets/chosen-sprite@2x.png b/assets/chosen-sprite@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ffe4d7d1121ca45cd35783696940050c690a8676 GIT binary patch literal 872 zcmeAS@N?(olHy`uVBq!ia0vp^89?mC!3-o5mOWetq|OBRgt-3y{~yTgfB`oc01C8& zL0el}Lqh|Q1oD8A5J8A6G6@lfkU;ftMr&&;+$f+RkloVK0wm!8C|FWb0x`e2xj8g6 z6i7l;BAWwL4-tfu5G@ccP!{MmqnYOy06pVW666=m!0_*bfx(A_gav;C1PU5HKT!C5 z;g7@l4POuZ{>hQBP{j4uqq`I2WP9&Sn{}I8xJi*&v*QL&+Dw-Bf-CQLyLk0Ht2-;T zynum$iPO`?F{C2y?bYjfj|~J`6Xz~`vM4Ef+U{Vpli%)D%kKSey_RG0Bi9Xw4jtOb z@!)?~_%DVelO793W#Pl3vjdNCDTnGGb(^YY+BbF2;mJK!nq}@nBGoHxCn#7X*gdxr z+~ZQF6rpL6;MDP)JzehcJBL53AM-QU?@65Plc4-}1EW@`H@EEGrnFmTubZ~5iUzVn zrZYyBIm|RmW$3F@+wkq2)c>|mGh5Rv3%(p~zjStyruBl(TP2_51LpTH2noN*e&}J; z1;$+4EpJ(Mb+^1}Fv>ms%b`r}+KYy~zf5x(4?8l28%eNnEHzx({;NGP)_hX&tEw9t zSKv=Qjt(d#~b;F7hbDdUu$OUK8iumkkl&-=~$APCd2zdqHg6@=Z#Pt*5RY_s@Hw*YS9+ z^u)7AmzM6Ux!dus_!gJY)Vtf?+t>a3wO2S)_Vct=T}#@RuG(h!d8WYZS9Tst+VfKv zt~zntPsvfyqLY`klZSPslCz_dlcSRJLa$Px)_+W2Y&u!?oJrIMCNTz2S3j3^P6") @@ -23,7 +25,7 @@ function M.mail:set_rcpt(rcpt) end -- set the from address -function M.mail:set_from(from) +function mail:set_from(from) local addr = validate_email(from) if addr then self.from = "<"..addr..">" @@ -32,33 +34,33 @@ function M.mail:set_from(from) end -- set the to address -function M.mail:set_to(to) +function mail:set_to(to) if validate_email(to) then self.headers.to = to end end -- set the cc address -function M.mail:set_cc(cc) +function mail:set_cc(cc) if validate_email(cc) then self.headers.cc = cc end end -- set the subject -function M.mail:set_subject(subject) +function mail:set_subject(subject) if (type(subject) == "string") then self.headers.subject = subject end end -- set the body -function M.mail:set_body(body) +function mail:set_body(body) self.body = body end -- send the email, and if failed return the error msg -function M.mail:send() +function mail:send() r, e = smtp.send{ from = self.from, rcpt = self.rcpt, @@ -66,12 +68,12 @@ function M.mail:send() headers = self.headers, body = self.body }), - server = conf.mail.server, - domain = conf.mail.domain + server = self.server, + domain = self.domain } if not r then return e end end -return M.mail() \ No newline at end of file +return mail \ No newline at end of file diff --git a/model.lua b/model.lua new file mode 100644 index 0000000..5f72ae0 --- /dev/null +++ b/model.lua @@ -0,0 +1,234 @@ +local model = class('model') + +-- keys used in alpine linux repository index +function model:indexFormat(k) + local f = { + P = "name", + V = "version", + T = "description", + U = "url", + L = "license", + A = "arch", + D = "depends", + C = "checksum", + S = "size", + I = "installed_size", + p = "provides", + i = "install_if", + o = "origin", + m = "maintainer", + t = "build_time", + c = "commit", + } + return k and f[k] or f +end + +-- package format as stored in database +function model:packageFormat(k) + local f = self:indexFormat() + f.r = "repo" + f.b = "branch" + return k and f[k] or f +end + +-- what happends here with version? +function model:packages(pkgs) + local r = {} + for k,v in pairs(pkgs) do + r[k] = {} + r[k].name = { + path=string.format("/package/%s/%s/%s/%s", v.branch, v.repo, v.arch, v.name), + text=v.name, + title=v.description + } + r[k].version = { + path=string.format("/flag/%s/%s/%s/%s", v.branch, v.repo, v.origin, v.version), + text=v.version, + title="Flag this package out of date" + } + r[k].url = { + path=v.url, + text="URL", + title=v.url + } + r[k].license = v.license + r[k].branch = v.branch + r[k].arch = v.arch + r[k].repo = v.repo + r[k].maintainer = v.mname or "None" + r[k].build_time = v.build_time + if (v.fid) then r[k].flagged = {date=v.flagged} end + end + return r +end + +function model:packagesForm(args, distinct) + local m = {} + m.name = args.name + m.branch = self:FormSelect(distinct.branch, args.branch) + m.repo = self:FormSelect(distinct.repo, args.repo) + m.arch = self:FormSelect(distinct.arch, args.arch) + m.maintainer = self:FormSelect(distinct.maintainer, args.maintainer) + return m +end + +function model:FormSelect(options, selected) + local r = {} + table.insert(options, 1, "") + for k,v in pairs(options) do + r[k] = {text=v} + if (v==selected) then r[k].selected = "selected" end + end + return r +end + +function model:package(pkg) + local r = {} + -- populate default values or None if not set. + for _,v in pairs(self:packageFormat()) do + r[v] = cntrl:isSet(pkg[v]) or "None" + end + r.nav = {package="active"} + r.version = {text=pkg.version} + if pkg.fid then + r.version.class = "text-danger" + r.version.title = string.format("Flagged: %s", pkg.flagged) + r.version.path = "#" + else + r.version.class = "text-success" + r.version.title = string.format("Flag this package out of date") + r.version.path = string.format("/flag/%s/%s/%s/%s",pkg.branch, pkg.repo, pkg.origin, pkg.version) + end + r.maintainer = pkg.mname or "None" + r.origin = { + path=string.format("/package/%s/%s/%s/%s", pkg.branch, pkg.repo, pkg.arch, pkg.origin), + text=pkg.origin + } + r.commit = { + path=string.format(conf.giturl, pkg.commit), + text=pkg.commit + } + r.contents = { + path=string.format("/contents?branch=%s&name=%s&arch=%s&repo=%s", pkg.branch, pkg.name, pkg.arch, pkg.repo), + text="Contents of package" + } + return r +end + +function model:flagged(pkgs) + local r = {} + for k,v in pairs(pkgs) do + r[k] = {} + r[k].name = { + path=string.format("/package/%s/%s/%s/%s", v.branch, v.repo, v.arch, v.name), + text=v.name, + title=v.description + } + r[k].version = v.version + r[k].new_version = v.new_version + r[k].branch = v.branch + r[k].arch = v.arch + r[k].repo = v.repo + r[k].maintainer = v.mname or "None" + r[k].created = v.created + r[k].message = v.message + end + return r +end + +function model:flaggedForm(args, distinct) + local m = {} + m.name = args.name + m.branch = model:FormSelect(distinct.branch, args.branch) + m.repo = model:FormSelect(distinct.repo, args.repo) + m.arch = model:FormSelect(distinct.arch, args.arch) + m.maintainer = model:FormSelect(distinct.maintainer, args.maintainer) + return m +end + +function model:packageRelations(pkgs) + local r = {} + for k,v in pairs(pkgs) do + local path = string.format("/package/%s/%s/%s/%s", v.branch, v.repo, v.arch, v.name) + table.insert(r, {path=path, text=v.name}) + end + return r +end + +function model:pagerModel(args, pager) + local result = {} + if pager.last > 1 then + table.insert(pager, 1, "«") + table.insert(pager, "»") + for k,p in ipairs(pager) do + local r = {} + for g,v in pairs(args) do + if (g=="page") then + if p == "«" then + v=1 + elseif p == "»" then + v=pager.last + else + v=p + end + end + if v ~= "" then + r[#r+1]=string.format("%s=%s", cntrl:urlEncode(g), cntrl:urlEncode(v)) + end + end + path = table.concat(r, '&') + class = (args.page == p) and "active" or "" + table.insert(result, {args=path, class=class, page=p}) + end + end + return result +end + +function model:contents(cnt) + local r = {} + for k,v in pairs(cnt) do + r[k] = {} + r[k].branch = v.branch + r[k].repo = v.repo + r[k].arch = v.arch + r[k].file = string.format("%s/%s", v.path, v.file) + r[k].pkgname = {} + r[k].pkgname.path = string.format("/package/%s/%s/%s/%s", v.branch, v.repo, v.arch, v.name) + r[k].pkgname.text = v.name + end + return r +end + +function model:contentsForm(args, distinct) + local m = {} + m.file = args.file + m.path = args.path + m.name = args.name + m.branch = self:FormSelect(distinct.branch, args.branch) + m.repo = self:FormSelect(distinct.repo, args.repo) + m.arch = self:FormSelect(distinct.arch, args.arch) + return m +end + +function model:flag(pkg, m) + m = m or {} + m.form = m.form or {} + m.nav = {flagged="active"} + m.repo = pkg.repo + m.origin = pkg.origin + m.version = pkg.version + m.maintainer = pkg.mname or "None" + m.sitekey = conf.rc.sitekey + return m +end + +function model:flagMail(pkg, args) + return { + maintainer = pkg.mname, + origin = pkg.origin, + from = args.from, + message = args.message + } +end + +return model() \ No newline at end of file diff --git a/tools/generate_filelist.lua b/tools/generate_filelist.lua deleted file mode 100755 index 4a29294..0000000 --- a/tools/generate_filelist.lua +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env luajit - -local lfs = require("lfs") -local cjson = require("cjson") - -local src = "/var/www/localhost/htdocs/alpine/edge" -local dst = "/var/tmp/filelist" -local filelist = "/var/www/localhost/filelist" -local repos = {"main","testing", "community"} -local archs = {"x86","x86_64","armhf"} -local dirfmt = "%s/%s/%s" -local logging = false - -function build(repo, arch) - local src = dirfmt:format(src, repo, arch) - local dst = dirfmt:format(dst, repo, arch) - local tarfmt = "tar ztf '%s' > %s" - for apk in lfs.dir(src) do - if apk:match(".apk$") then - local lst = string.format("%s/%s.lst", dst, apk) - if not lfs.attributes(lst) then - local apk = string.format("%s/%s", src, apk) - os.execute(tarfmt:format(apk, lst)) - end - end - end -end - -function create_json(repo, arch) - local dst = dirfmt:format(dst, repo, arch) - local src = dirfmt:format(src, repo, arch) - local result = {} - for lst in lfs.dir(dst) do - if lst:match("%.lst$") then - local pkgname = lst:match("(.*)%-.*%-.*$") - local r = {} - local f = io.open(dst.."/"..lst) - for line in f:lines() do - if not (line:match("^%.") or line:match("/$")) then - local path, file - if line:match("/") then - path, file = line:match("(.*/)(.*)") - if path:sub(-1) == "/" then - path = (path:sub(1, -2)) - end - else - path = "" - file = line - end - table.insert(r, {file, path}) - end - end - f:close() - result[pkgname] = r - end - end - local json = string.format("%s/%s-%s.json", filelist, repo, arch) - os.execute("rm -f "..json..".gz") - local g = io.open(json, "w") - g:write(cjson.encode(result)) - g:close() - os.execute("gzip "..json) -end - -function clean(repo, arch) - local dst = dirfmt:format(dst, repo, arch) - local src = dirfmt:format(src, repo, arch) - for lst in lfs.dir(dst) do - local apk = lst:sub(1,-5) - if not lfs.attributes(src.."/"..apk) then - os.remove(dst.."/"..lst) - end - end -end - -function index_changed(repo, arch) - local json = string.format("%s/%s-%s.json.gz", filelist, repo, arch) - local index = string.format("%s/%s/%s/APKINDEX.tar.gz", src, repo, arch) - local index_attr = lfs.attributes(index) - local json_attr = lfs.attributes(json) - if (json_attr == nil) or (index_attr.modification > json_attr.modification) then - return true - end -end - -function log(msg) - if logging then - if logging == "syslog" then - os.execute("logger "..msg) - else - print(msg) - end - end -end - -for _,repo in ipairs(repos) do - lfs.mkdir(dst.."/"..repo) - for _,arch in ipairs(archs) do - if index_changed(repo, arch) then - lfs.mkdir(dirfmt:format(dst, repo, arch)) - log(string.format("Building %s/%s", repo, arch)) - build(repo, arch) - log(string.format("Cleaning %s/%s", repo, arch)) - clean(repo, arch) - log(string.format("Create json %s/%s", repo, arch)) - create_json(repo, arch) - else - log(string.format("No changes found for %s/%s", repo, arch)) - end - end -end diff --git a/tools/import.lua b/tools/import.lua new file mode 100644 index 0000000..d01218a --- /dev/null +++ b/tools/import.lua @@ -0,0 +1,393 @@ +local sqlite = require("lsqlite3") +local class = require("middleclass") + +local conf = require("config") + +--- +-- import +--- + +local import = class("import") + +function import:initialize() + self.db = sqlite.open(conf.db.path) + self.db:exec("PRAGMA foreign_keys=ON") + self.db:exec("PRAGMA journal_mode=WAL") + if conf.db.init then self:createTables() end +end + +function import:finalize() + self.db:close() +end + +function import:split(d,s) + local r = {} + for i in s:gmatch(d) do table.insert(r,i) end + return r +end + +function import:log(msg) + if conf.logging then + if conf.logging == "syslog" then + os.execute("logger "..msg) + else + print(msg) + end + end +end + +function import:fileExists(path) + local f = io.open(path, "r") + if f ~= nil then + io.close(f) + return true + end + return false +end + +-- keys used in alpine linux repository index +function import:indexFormat(k) + local f = { + P = "name", + V = "version", + T = "description", + U = "url", + L = "license", + A = "arch", + D = "depends", + C = "checksum", + S = "size", + I = "installed_size", + p = "provides", + i = "install_if", + o = "origin", + m = "maintainer", + t = "build_time", + c = "commit", + } + return k and f[k] or f +end + +function import:createTables() + self.db:exec( [[ CREATE TABLE IF NOT EXISTS 'packages' ( + 'id' INTEGER primary key, + 'name' TEXT, + 'version' TEXT, + 'description' TEXT, + 'url' TEXT, + 'license' TEXT, + 'arch' TEXT, + 'branch' TEXT, + 'repo' TEXT, + 'checksum' TEXT, + 'size' INTEGER, + 'installed_size' INTEGER, + 'origin' TEXT, + 'maintainer' INTEGER, + 'build_time' INTEGER, + 'commit' TEXT, + 'fid' INTEGER + ) ]]) + self.db:exec([[ CREATE INDEX IF NOT EXISTS 'packages_name' on 'packages' (name) ]]) + self.db:exec([[ CREATE INDEX IF NOT EXISTS 'packages_maintainer' on 'packages' (maintainer) ]]) + self.db:exec([[ CREATE INDEX IF NOT EXISTS 'packages_build_time' on 'packages' (build_time) ]]) + self.db:exec([[ CREATE TABLE IF NOT EXISTS 'files' ( + 'id' INTEGER primary key, + 'file' TEXT, + 'path' TEXT, + 'pkgname' TEXT, + 'pid' INTEGER REFERENCES packages(id) ON DELETE CASCADE + ) ]]) + self.db:exec([[ CREATE INDEX IF NOT EXISTS 'files_file' on 'files' (file) ]]) + self.db:exec([[ CREATE INDEX IF NOT EXISTS 'files_path' on 'files' (path) ]]) + self.db:exec([[ CREATE INDEX IF NOT EXISTS 'files_pkgname' on 'files' (pkgname) ]]) + self.db:exec([[ CREATE INDEX IF NOT EXISTS 'files_pid' on 'files' (pid) ]]) + local field = [[ CREATE TABLE IF NOT EXISTS '%s' ( + 'name' TEXT, + 'version' TEXT, + 'operator' TEXT, + 'pid' INTEGER REFERENCES packages(id) ON DELETE CASCADE + )]] + for _,v in pairs(conf.db.fields) do + self.db:exec(string.format(field,v)) + self.db:exec(string.format([[CREATE INDEX IF NOT EXISTS '%s_name' on '%s' (name)]], v, v)) + self.db:exec(string.format([[CREATE INDEX IF NOT EXISTS '%s_pid' on '%s' (pid)]], v, v)) + end + self.db:exec([[CREATE TABLE IF NOT EXISTS maintainer ( + 'id' INTEGER primary key, + 'name' TEXT, + 'email' TEXT + )]]) + self.db:exec([[CREATE INDEX IF NOT EXISTS 'maintainer_name' + on maintainer (name)]]) + self.db:exec([[ CREATE TABLE IF NOT EXISTS 'repoversion' ( + 'branch' TEXT, + 'repo' TEXT, + 'arch' TEXT, + 'version' TEXT + )]]) + self.db:exec([[CREATE UNIQUE INDEX IF NOT EXISTS 'repoversion_version' + on repoversion (branch, repo, arch)]]) + self.db:exec([[ CREATE TABLE IF NOT EXISTS 'flagged' ( + 'fid' INTEGER primary key, + 'created' INTEGER, + 'reporter' TEXT, + 'new_version' TEXT, + 'message' TEXT + ) ]]) +end + +--- +-- get the current git describe from DESCRIPTION file +--- +function import:getRepoVersion(branch, repo, arch) + local index = string.format("%s/%s/%s/%s/APKINDEX.tar.gz", + conf.mirror, branch, repo, arch) + local f = io.popen(string.format("tar -Ozx -f '%s' DESCRIPTION", index)) + for line in f:lines() do + return line + end +end + +function import:getLocalRepoVersion(branch, repo, arch) + local sql = [[ select version from repoversion + where branch = ? and repo = ? and arch = ? ]] + local stmt = self.db:prepare(sql) + stmt:bind_values(branch, repo, arch) + local r = (stmt:step()==sqlite3.ROW) and stmt:get_value(0) or false + stmt:finalize() + return r +end + +function import:updateLocalRepoVersion(branch, repo, arch, version) + local sql = [[ insert or replace into repoversion ('version', 'branch', 'repo', 'arch') + VALUES (:version, :branch, :repo, :arch) ]] + local stmt = self.db:prepare(sql) + stmt:bind_names({version=version,branch=branch,repo=repo,arch=arch}) + stmt:step() + stmt:finalize() +end + +--- +-- compare remote repo version with local and return remote version if updated +--- +function import:repoUpdated(branch, repo, arch) + local v = self:getRepoVersion(branch, repo, arch) + local l = self:getLocalRepoVersion(branch, repo, arch) + if (v ~= l) then return v end +end + +function import:getApkIndex(branch, repo, arch) + local r,i = {},{} + local index = string.format("%s/%s/%s/%s/APKINDEX.tar.gz", + conf.mirror, branch, repo, arch) + local f = io.popen(string.format("tar -Ozx -f '%s' APKINDEX", index)) + for line in f:lines() do + if (line ~= "") then + local k,v = line:match("^(%a):(.*)") + local key = self:indexFormat(k) + r[key] = k:match("^[Dpi]$") and self:split("%S+", v) or v + else + local nv = string.format("%s-%s", r.name, r.version) + r.repo = repo + r.branch = branch + i[nv] = r + r = {} + end + end + f:close() + return i +end + +function import:getChanges(branch, repo, arch) + local del = {} + local add = self:getApkIndex(branch, repo, arch) + local sql = [[SELECT branch, repo, arch, name,version FROM 'packages' + WHERE branch = ? + AND repo = ? + AND arch = ? + ]] + local stmt = self.db:prepare(sql) + stmt:bind_values(branch,repo,arch) + for r in stmt:nrows() do + local nv = string.format("%s-%s", r.name, r.version) + if add[nv] then + add[nv] = nil + else + del[nv] = r + end + end + stmt:finalize() + return add,del +end + +function import:addPackages(branch, add) + for _,pkg in pairs(add) do + local apk = string.format("%s/%s/%s/%s/%s-%s.apk", + conf.mirror, branch, pkg.repo, pkg.arch, pkg.name, pkg.version) + if self:fileExists(apk) then + self:log(string.format("Adding: %s/%s/%s/%s-%s", branch, pkg.repo, pkg.arch, pkg.name, pkg.version)) + pkg.maintainer = self:addMaintainer(pkg.maintainer) + local pid = self:addHeader(pkg) + self:addFields(pid, pkg) + self:addFiles(pid, apk, pkg) + else + self:log(string.format("Could not find pkg: %s/%s/%s/%s-%s", branch, pkg.repo, pkg.arch, pkg.name, pkg.version)) + end + end +end + +function import:addHeader(pkg) + local sql = [[ insert into 'packages' ("name", "version", "description", "url", + "license", "arch", "branch", "repo", "checksum", "size", "installed_size", "origin", + "maintainer", "build_time", "commit") values(:name, :version, :description, + :url, :license, :arch, :branch, :repo, :checksum, :size, :installed_size, :origin, + :maintainer, :build_time, :commit)]] + local stmt = self.db:prepare(string.format(sql)) + stmt:bind_names(pkg) + stmt:step() + local pid = stmt:last_insert_rowid() + stmt:finalize() + return pid +end + +function import:formatMaintainer(maintainer) + if maintainer then + local r = {} + maintainer = maintainer:match("^%s*(.-)%s*$") + local name,email = maintainer:match("(.*)(<.*>)") + r.email = email:match("[A-Za-z0-9%.%%%+%-]+@[A-Za-z0-9%.%%%+%-]+%.%w%w%w?%w?") + if r.email then + r.name = name:match("^%s*(.-)%s*$") + return r + end + end +end + +function import:addMaintainer(maintainer) + local m = self:formatMaintainer(maintainer) + if m then + local sql = [[ insert or replace into maintainer ('id', 'name', 'email') + VALUES ((SELECT id FROM maintainer WHERE name = :name AND email = :email), + :name, :email) ]] + local stmt = self.db:prepare(sql) + stmt:bind_names(m) + stmt:step() + local r = stmt:last_insert_rowid() + stmt:reset() + stmt:finalize() + return r + end +end + +function import:delPackages(branch, del) + local sql = [[ delete FROM 'packages' WHERE "branch" = :branch + AND "repo" = :repo AND "arch" = :arch AND "name" = :name + AND "version" = :version ]] + local stmt = self.db:prepare(sql) + for _,pkg in pairs(del) do + self:log(string.format("Deleting: %s/%s/%s/%s-%s", branch, pkg.repo, pkg.arch, pkg.name, pkg.version)) + stmt:bind_names(pkg) + stmt:step() + stmt:reset() + end + stmt:finalize() +end + +function import:formatField(v) + local r = {} + for _,o in ipairs({">=","<=","><","=",">","<"}) do + if v:match(o) then + r.name,r.version = v:match("^(.*)"..o.."(.*)$") + r.operator = o + return r + end + end + r.name = v + return r +end + +function import:addFields(pid, pkg) + for _,field in ipairs(conf.db.fields) do + local values = pkg[field] or {} + --insert pkg name as a provides in the table. + if field == "provides" then table.insert(values, pkg.name) end + local sql = [[ insert into '%s' ("pid", "name", "version", "operator") + VALUES (:pid, :name, :version, :operator) ]] + local stmt = self.db:prepare(string.format(sql, field)) + for _,v in pairs(values) do + local r = self:formatField(v) + r.pid = pid + stmt:bind_names(r) + stmt:step() + stmt:reset() + end + stmt:finalize() + end +end + +function import:getFilelist(apk) + r = {} + local f = io.popen(string.format("tar ztf '%s'", apk)) + for line in f:lines() do + if not (line:match("^%.") or line:match("/$")) then + local path,file = self:formatFile(line) + table.insert(r, {path=path,file=file}) + end + end + f:close() + return r +end + +function import:addFiles(pid, apk, pkg) + local files = self:getFilelist(apk) + local sql = [[ insert into 'files' ("file", "path", "pkgname", "pid") + VALUES (:file, :path, :pkgname, :pid) ]] + local stmt = self.db:prepare(sql) + for _,file in pairs(files) do + file.pkgname = pkg.name + file.pid = pid + stmt:bind_names(file) + local step = stmt:step() + stmt:reset() + end + stmt:finalize() +end + +function import:formatFile(line) + local path, file + if line:match("/") then + path, file = line:match("(.*/)(.*)") + if path:match("/$") then path = path:sub(1, -2) end + return "/"..path,file + end + return "/", line +end + +function import:run() + for _,branch in pairs(conf.branches) do + for _,repo in pairs(conf.repos) do + for _,arch in pairs(conf.archs) do + local index = string.format("%s/%s/%s/%s/APKINDEX.tar.gz", + conf.mirror, branch, repo, arch) + if self:fileExists(index) then + local version = self:repoUpdated(branch, repo, arch) + if version then + self:log(string.format("Updating: %s/%s/%s",branch, repo, arch)) + local add,del = self:getChanges(branch, repo, arch) + self.db:exec("begin transaction") + self:addPackages(branch, add) + self:delPackages(branch, del) + self.db:exec("commit") + self:updateLocalRepoVersion(branch, repo, arch, version) + end + end + end + end + end + self:log("Update finished.") +end + +local import = import() +import:run() +import:finalize() diff --git a/tpl/contents.tpl b/tpl/contents.tpl deleted file mode 100644 index 0a9dc9f..0000000 --- a/tpl/contents.tpl +++ /dev/null @@ -1,72 +0,0 @@ -{{{header}}} -
-
-
Search the contents of packages
-
- -
-
- - - - - - - - {{#contents}} - - - - - - - {{/contents}} - {{^contents}} - - - - {{/contents}} -
FilePackageRepositoryArchitecture
{{file}}{{pkgname.text}}{{repo}}{{arch}}
No item found...
-
- -
-
-{{{footer}}} diff --git a/tpl/flagged.tpl b/tpl/flagged.tpl deleted file mode 100644 index 092cce8..0000000 --- a/tpl/flagged.tpl +++ /dev/null @@ -1,61 +0,0 @@ -{{{header}}} - -
-
-
Search for flagged packages
-
- -
-
- - - - - - - - - {{#pkgs}} - - - - - - - - {{/pkgs}} - {{^pkgs}} - - - - {{/pkgs}} -
OriginRepositoryVersionDateMessage
- {{origin.text}} - {{repo}}{{version}}{{date}}
No item found...
-
- -
-
-{{{footer}}} diff --git a/views/contents.tpl b/views/contents.tpl new file mode 100644 index 0000000..4fc5c08 --- /dev/null +++ b/views/contents.tpl @@ -0,0 +1,91 @@ +{{{header}}} + +
+
+
Search the contents of packages
+
+ +
+
+ + + + + + + + + {{#contents}} + + + + + + + + {{/contents}} + {{^contents}} + + + + {{/contents}} +
FilePackageBranchRepositoryArchitecture
{{{file}}}{{{pkgname.text}}}{{{branch}}}{{{repo}}}{{{arch}}}
No item found...
+
+ +
+
+{{{footer}}} diff --git a/tpl/flag.tpl b/views/flag.tpl similarity index 65% rename from tpl/flag.tpl rename to views/flag.tpl index 0890090..7a8a5cb 100644 --- a/tpl/flag.tpl +++ b/views/flag.tpl @@ -10,20 +10,23 @@
Your information
-
+
- +

Your email address the developer can contact you on.

- -
+
+ + +

The new version number from upstream.

+
+
- -

Leave a message to the developer.

{{#sitekey}} + +

Leave a message to the developer.

{{{#sitekey}}} -
{{/sitekey}} +
{{{/sitekey}}}
-
@@ -35,13 +38,13 @@
- - - - + + + +
Origin name{{origin}}
Repository{{repo}}
Version{{version}}
Maintainer{{maintainer}}
Origin name{{{origin}}}
Repository{{{repo}}}
Version{{{version}}}
Maintainer{{{maintainer}}}
- +
diff --git a/views/flagged.tpl b/views/flagged.tpl new file mode 100644 index 0000000..5cf1c88 --- /dev/null +++ b/views/flagged.tpl @@ -0,0 +1,98 @@ +{{{header}}} + +
+
+
Search for packages
+
+ +
+
+ + + + + + + + + + + + + {{#pkgs}} + + + + + + + + + + + + {{/pkgs}} + {{^pkgs}} + + + + {{/pkgs}} +
PackageVersionNew versionBranchRepositoryArchitectureMaintainerFlag dateMessage
+ {{{name.text}}} + {{{version}}}{{{new_version}}}{{{branch}}}{{{repo}}}{{{arch}}}{{{maintainer}}}{{{created}}} + +
No item found...
+
+ +
+
+{{{footer}}} diff --git a/tpl/footer.tpl b/views/footer.tpl similarity index 81% rename from tpl/footer.tpl rename to views/footer.tpl index af20bfe..fdcdcad 100644 --- a/tpl/footer.tpl +++ b/views/footer.tpl @@ -1,5 +1,5 @@
-
© Copyright 2014 Alpine Linux Development Team all rights reserved | Privacy Policy
+
© Copyright 2016 Alpine Linux Development Team all rights reserved | Privacy Policy
Please report issues for this service to its Github project page
diff --git a/tpl/header.tpl b/views/header.tpl similarity index 93% rename from tpl/header.tpl rename to views/header.tpl index df682a3..0059074 100644 --- a/tpl/header.tpl +++ b/views/header.tpl @@ -8,6 +8,7 @@ + @@ -29,6 +30,7 @@ +
@@ -89,7 +99,7 @@
@@ -101,7 +111,7 @@
    - {{#reqbys}}
  • {{text}}
  • {{/reqbys}} + {{#reqbys}}
  • {{{text}}}
  • {{/reqbys}} {{^reqbys}}
  • None
  • {{/reqbys}}
@@ -113,7 +123,7 @@
    - {{#subpkgs}}
  • {{text}}
  • {{/subpkgs}} + {{#subpkgs}}
  • {{{text}}}
  • {{/subpkgs}} {{^subpkgs}}
  • None
  • {{/subpkgs}}
diff --git a/tpl/packages.tpl b/views/packages.tpl similarity index 57% rename from tpl/packages.tpl rename to views/packages.tpl index 788a9a3..5a2457e 100644 --- a/tpl/packages.tpl +++ b/views/packages.tpl @@ -2,7 +2,7 @@
@@ -11,30 +11,36 @@